diff --git a/MeAjudaAi.sln b/MeAjudaAi.sln index d855166a9..875a8b137 100644 --- a/MeAjudaAi.sln +++ b/MeAjudaAi.sln @@ -123,7 +123,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastru EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Locations.Infrastructure", "src\Modules\Locations\Infrastructure\MeAjudaAi.Modules.Locations.Infrastructure.csproj", "{1E72996A-6FD1-9E16-5188-42DDCFF6518C}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Search", "Search", "{6FF68FBA-C4AF-48EC-AFE2-E320F2195C79}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SearchProviders", "SearchProviders", "{6FF68FBA-C4AF-48EC-AFE2-E320F2195C79}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "API", "API", "{977BE21B-C0F0-4625-9C9D-8A5A6D8C2D49}" EndProject @@ -163,6 +163,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "API", "API", "{30A2D3C4-AF9 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.ServiceCatalogs.API", "src\Modules\ServiceCatalogs\API\MeAjudaAi.Modules.ServiceCatalogs.API.csproj", "{B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{BDD25844-1435-F5BA-1F9B-EFB3B12C916F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.ServiceCatalogs.Tests", "src\Modules\ServiceCatalogs\Tests\MeAjudaAi.Modules.ServiceCatalogs.Tests.csproj", "{2C85E336-66A2-4B4F-845A-DBA2A6520162}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -617,6 +621,18 @@ Global {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Release|x64.Build.0 = Release|Any CPU {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Release|x86.ActiveCfg = Release|Any CPU {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Release|x86.Build.0 = Release|Any CPU + {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Debug|x64.ActiveCfg = Debug|Any CPU + {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Debug|x64.Build.0 = Debug|Any CPU + {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Debug|x86.ActiveCfg = Debug|Any CPU + {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Debug|x86.Build.0 = Debug|Any CPU + {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Release|Any CPU.Build.0 = Release|Any CPU + {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Release|x64.ActiveCfg = Release|Any CPU + {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Release|x64.Build.0 = Release|Any CPU + {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Release|x86.ActiveCfg = Release|Any CPU + {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -697,6 +713,8 @@ Global {3B6D6C13-1E04-47B9-B44E-36D25DF913C7} = {8D23D6D3-2B2E-7F09-866F-FA51CC0FC081} {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8} = {8B551008-B254-EBAF-1B6D-AB7C420234EA} {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E} = {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8} + {BDD25844-1435-F5BA-1F9B-EFB3B12C916F} = {8B551008-B254-EBAF-1B6D-AB7C420234EA} + {2C85E336-66A2-4B4F-845A-DBA2A6520162} = {BDD25844-1435-F5BA-1F9B-EFB3B12C916F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {391B5342-8EC5-4DF0-BCDA-6D73F87E8751} diff --git a/docs/architecture.md b/docs/architecture.md index 41d119e1e..aaa87d7af 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -115,7 +115,7 @@ public class ProvidersContext - **Qualification**: Qualificações e habilitações profissionais - **VerificationStatus**: Status de verificação (Pending, Verified, Rejected, etc.) -#### 3. **Catalogs Context** (Implementado) +#### 3. **ServiceCatalogs Context** (Implementado) **Responsabilidade**: Catálogo administrativo de categorias e serviços **Conceitos Implementados**: @@ -124,7 +124,7 @@ public class ProvidersContext - **DisplayOrder**: Ordenação customizada para apresentação - **Activation/Deactivation**: Controle de visibilidade no catálogo -**Schema**: `catalogs` (isolado no PostgreSQL) +**Schema**: `service_catalogs` (isolado no PostgreSQL) #### 4. **Location Context** (Implementado) **Responsabilidade**: Geolocalização e lookup de CEP brasileiro diff --git a/docs/modules/catalogs.md b/docs/modules/service_catalogs.md similarity index 88% rename from docs/modules/catalogs.md rename to docs/modules/service_catalogs.md index fa9e43104..4f8e1d6f9 100644 --- a/docs/modules/catalogs.md +++ b/docs/modules/service_catalogs.md @@ -1,10 +1,10 @@ -# 📋 Módulo Catalogs - Catálogo de Serviços +# 📋 Módulo service_catalogs - Catálogo de Serviços > **✅ Status**: Módulo **implementado e funcional** (Novembro 2025) ## 🎯 Visão Geral -O módulo **Catalogs** é responsável pelo **catálogo administrativo de serviços** oferecidos na plataforma MeAjudaAi, implementando um Bounded Context dedicado para gestão hierárquica de categorias e serviços. +O módulo **service_catalogs** é responsável pelo **catálogo administrativo de serviços** oferecidos na plataforma MeAjudaAi, implementando um Bounded Context dedicado para gestão hierárquica de categorias e serviços. ### **Responsabilidades** - ✅ **Catálogo hierárquico** de categorias de serviços @@ -16,8 +16,8 @@ O módulo **Catalogs** é responsável pelo **catálogo administrativo de servi ## 🏗️ Arquitetura Implementada -### **Bounded Context: Catalogs** -- **Schema**: `catalogs` (isolado no PostgreSQL) +### **Bounded Context: service_catalogs** +- **Schema**: `service_catalogs` (isolado no PostgreSQL) - **Padrão**: DDD + CQRS - **Naming**: snake_case no banco, PascalCase no código @@ -197,10 +197,10 @@ POST /api/v1/catalogs/services/validate # Validar batch de serviço ## 🔌 Module API - Comunicação Inter-Módulos -### **Interface ICatalogsModuleApi** +### **Interface Iservice_catalogsModuleApi** ```csharp -public interface ICatalogsModuleApi : IModuleApi +public interface Iservice_catalogsModuleApi : IModuleApi { // Service Categories Task> GetServiceCategoryByIdAsync( @@ -266,11 +266,11 @@ public sealed record ModuleServiceValidationResultDto( ```csharp [ModuleApi(ModuleMetadata.Name, ModuleMetadata.Version)] -public sealed class CatalogsModuleApi : ICatalogsModuleApi +public sealed class service_catalogsModuleApi : Iservice_catalogsModuleApi { private static class ModuleMetadata { - public const string Name = "Catalogs"; + public const string Name = "service_catalogs"; public const string Version = "1.0"; } @@ -292,11 +292,11 @@ public sealed class CatalogsModuleApi : ICatalogsModuleApi ## 🗄️ Schema de Banco de Dados ```sql --- Schema: catalogs -CREATE SCHEMA IF NOT EXISTS catalogs; +-- Schema: service_catalogs +CREATE SCHEMA IF NOT EXISTS service_catalogs; -- Tabela: service_categories -CREATE TABLE catalogs.service_categories ( +CREATE TABLE service_catalogs.service_categories ( id UUID PRIMARY KEY, name VARCHAR(100) NOT NULL UNIQUE, description VARCHAR(500), @@ -309,9 +309,9 @@ CREATE TABLE catalogs.service_categories ( ); -- Tabela: services -CREATE TABLE catalogs.services ( +CREATE TABLE service_catalogs.services ( id UUID PRIMARY KEY, - category_id UUID NOT NULL REFERENCES catalogs.service_categories(id), + category_id UUID NOT NULL REFERENCES service_catalogs.service_categories(id), name VARCHAR(150) NOT NULL UNIQUE, description VARCHAR(1000), is_active BOOLEAN NOT NULL DEFAULT TRUE, @@ -323,11 +323,11 @@ CREATE TABLE catalogs.services ( ); -- Índices -CREATE INDEX idx_services_category_id ON catalogs.services(category_id); -CREATE INDEX idx_services_is_active ON catalogs.services(is_active); -CREATE INDEX idx_service_categories_is_active ON catalogs.service_categories(is_active); -CREATE INDEX idx_service_categories_display_order ON catalogs.service_categories(display_order); -CREATE INDEX idx_services_display_order ON catalogs.services(display_order); +CREATE INDEX idx_services_category_id ON service_catalogs.services(category_id); +CREATE INDEX idx_services_is_active ON service_catalogs.services(is_active); +CREATE INDEX idx_service_categories_is_active ON service_catalogs.service_categories(is_active); +CREATE INDEX idx_service_categories_display_order ON service_catalogs.service_categories(display_order); +CREATE INDEX idx_services_display_order ON service_catalogs.services(display_order); ``` ## 🔗 Integração com Outros Módulos @@ -342,7 +342,7 @@ public class Provider public class ProviderService { - public Guid ServiceId { get; set; } // FK para Catalogs.Service + public Guid ServiceId { get; set; } // FK para service_catalogs.Service public decimal Price { get; set; } public bool IsOffered { get; set; } } @@ -360,13 +360,13 @@ public class SearchableProvider ## 📊 Estrutura de Pastas ```plaintext -src/Modules/Catalogs/ +src/Modules/ServiceCatalogs/ ├── API/ │ ├── Endpoints/ │ │ ├── ServiceCategoryEndpoints.cs │ │ ├── ServiceEndpoints.cs -│ │ └── CatalogsModuleEndpoints.cs -│ └── MeAjudaAi.Modules.Catalogs.API.csproj +│ │ └── service_catalogsModuleEndpoints.cs +│ └── MeAjudaAi.Modules.service_catalogs.API.csproj ├── Application/ │ ├── Commands/ │ │ ├── Service/ # 6 commands @@ -381,8 +381,8 @@ src/Modules/Catalogs/ │ │ └── QueryHandlers.cs # 6 handlers consolidados │ ├── DTOs/ # 5 DTOs │ ├── ModuleApi/ -│ │ └── CatalogsModuleApi.cs -│ └── MeAjudaAi.Modules.Catalogs.Application.csproj +│ │ └── service_catalogsModuleApi.cs +│ └── MeAjudaAi.Modules.service_catalogs.Application.csproj ├── Domain/ │ ├── Entities/ │ │ ├── Service.cs @@ -398,10 +398,10 @@ src/Modules/Catalogs/ │ ├── ValueObjects/ │ │ ├── ServiceId.cs │ │ └── ServiceCategoryId.cs -│ └── MeAjudaAi.Modules.Catalogs.Domain.csproj +│ └── MeAjudaAi.Modules.service_catalogs.Domain.csproj ├── Infrastructure/ │ ├── Persistence/ -│ │ ├── CatalogsDbContext.cs +│ │ ├── service_catalogsDbContext.cs │ │ ├── Configurations/ │ │ │ ├── ServiceConfiguration.cs │ │ │ └── ServiceCategoryConfiguration.cs @@ -409,7 +409,7 @@ src/Modules/Catalogs/ │ │ ├── ServiceRepository.cs │ │ └── ServiceCategoryRepository.cs │ ├── Extensions.cs -│ └── MeAjudaAi.Modules.Catalogs.Infrastructure.csproj +│ └── MeAjudaAi.Modules.service_catalogs.Infrastructure.csproj └── Tests/ ├── Builders/ │ ├── ServiceBuilder.cs @@ -438,7 +438,7 @@ src/Modules/Catalogs/ - Domain events ### **Testes de Integração** -- ✅ **CatalogsIntegrationTests**: 29 testes passando +- ✅ **service_catalogsIntegrationTests**: 29 testes passando - Endpoints REST completos - Module API - Repository operations diff --git a/docs/roadmap.md b/docs/roadmap.md index 7431833c7..311cc1a68 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -218,15 +218,15 @@ public interface ILocationModuleApi : IModuleApi --- -### 1.6. ✅ Módulo Catalogs (Concluído) +### 1.6. ✅ Módulo ServiceCatalogs (Concluído) **Status**: Implementado e funcional com testes completos -**Objetivo**: Gerenciar tipos de serviços que prestadores podem oferecer através de um catálogo admin-managed. +**Objetivo**: Gerenciar tipos de serviços que prestadores podem oferecer por catálogo gerenciado administrativamente. #### **Arquitetura Implementada** - **Padrão**: DDD + CQRS com hierarquia de categorias -- **Schema**: `catalogs` (isolado) +- **Schema**: `service_catalogs` (isolado) - **Naming**: snake_case no banco, PascalCase no código #### **Entidades de Domínio Implementadas** @@ -275,10 +275,10 @@ public sealed class Service : AggregateRoot - Categories: GetById, GetAll, GetWithCount - Services: GetById, GetAll, GetByCategory - **Handlers**: 11 Command Handlers + 6 Query Handlers -- **Module API**: `CatalogsModuleApi` para comunicação inter-módulos +- **Module API**: `ServiceCatalogsModuleApi` para comunicação inter-módulos **3. Infrastructure Layer** ✅ -- `CatalogsDbContext` com schema isolation (`catalogs`) +- `ServiceCatalogsDbContext` com schema isolation (`service_catalogs`) - EF Core Configurations (snake_case, índices otimizados) - Repositories com SaveChangesAsync integrado - DI registration com auto-migration support @@ -305,12 +305,12 @@ public sealed class Service : AggregateRoot - **Versionamento**: Sistema unificado via BaseEndpoint **5. Shared.Contracts** ✅ -- `ICatalogsModuleApi` - Interface pública +- `IServiceCatalogsModuleApi` - Interface pública - DTOs: ModuleServiceCategoryDto, ModuleServiceDto, ModuleServiceListDto, ModuleServiceValidationResultDto #### **API Pública Implementada** ```csharp -public interface ICatalogsModuleApi : IModuleApi +public interface IServiceCatalogsModuleApi : IModuleApi { Task> GetServiceCategoryByIdAsync(Guid categoryId, CancellationToken ct = default); Task>> GetAllServiceCategoriesAsync(bool activeOnly = true, CancellationToken ct = default); @@ -336,7 +336,7 @@ public interface ICatalogsModuleApi : IModuleApi #### **Próximos Passos (Pós-MVP)** 1. **Testes**: Implementar unit tests e integration tests -2. **Migrations**: Criar e aplicar migration inicial do schema `catalogs` +2. **Migrations**: Criar e aplicar migration inicial do schema `service_catalogs` 3. **Bootstrap**: Integrar no Program.cs e AppHost 4. **Provider Integration**: Estender Providers para suportar ProviderServices 5. **Admin UI**: Interface para gestão de catálogo @@ -350,8 +350,8 @@ public interface ICatalogsModuleApi : IModuleApi #### **Schema do Banco de Dados** ```sql --- Schema: catalogs -CREATE TABLE catalogs.service_categories ( +-- Schema: service_catalogs +CREATE TABLE service_catalogs.service_categories ( id UUID PRIMARY KEY, name VARCHAR(200) NOT NULL UNIQUE, description TEXT, @@ -361,9 +361,9 @@ CREATE TABLE catalogs.service_categories ( updated_at TIMESTAMP ); -CREATE TABLE catalogs.services ( +CREATE TABLE service_catalogs.services ( id UUID PRIMARY KEY, - category_id UUID NOT NULL REFERENCES catalogs.service_categories(id), + category_id UUID NOT NULL REFERENCES service_catalogs.service_categories(id), name VARCHAR(200) NOT NULL UNIQUE, description TEXT, is_active BOOLEAN NOT NULL DEFAULT TRUE, @@ -372,9 +372,9 @@ CREATE TABLE catalogs.services ( updated_at TIMESTAMP ); -CREATE INDEX idx_services_category_id ON catalogs.services(category_id); -CREATE INDEX idx_services_is_active ON catalogs.services(is_active); -CREATE INDEX idx_service_categories_is_active ON catalogs.service_categories(is_active); +CREATE INDEX idx_services_category_id ON service_catalogs.services(category_id); +CREATE INDEX idx_services_is_active ON service_catalogs.services(is_active); +CREATE INDEX idx_service_categories_is_active ON service_catalogs.service_categories(is_active); ``` --- @@ -688,7 +688,7 @@ web/ **Funcionalidades Core**: - **User & Provider Management**: Visualizar, suspender, verificar manualmente -- **Catalogs Management**: Aprovar/rejeitar serviços sugeridos +- **ServiceCatalogs Management**: Aprovar/rejeitar serviços sugeridos - **Review Moderation**: Lidar com reviews sinalizados - **Dashboard**: Métricas-chave do módulo Analytics @@ -780,7 +780,7 @@ web/ 3. ✅ Módulo Documents (Concluído) 4. ✅ Módulo Search & Discovery (Concluído) 5. 📋 Módulo Location - CEP lookup e geocoding -6. 📋 Módulo Catalogs - Catálogo admin-managed de categorias e serviços +6. 📋 Módulo ServiceCatalogs - Catálogo admin-managed de categorias e serviços 7. 📋 Admin Portal - Gestão básica 8. 📋 Customer Profile - Gestão de perfil diff --git a/docs/testing/test_infrastructure.md b/docs/testing/test_infrastructure.md index c352771ce..c286c5bda 100644 --- a/docs/testing/test_infrastructure.md +++ b/docs/testing/test_infrastructure.md @@ -214,13 +214,13 @@ tests/MeAjudaAi.E2E.Tests/ ├── Modules/ │ ├── Users/ │ │ └── UsersEndToEndTests.cs # Testes E2E de Users -│ ├── Catalogs/ -│ │ └── CatalogsEndToEndTests.cs # Testes E2E de Catalogs +│ ├── ServiceCatalogs/ +│ │ └── ServiceCatalogsEndToEndTests.cs # Testes E2E de ServiceCatalogs │ └── Providers/ │ └── ProvidersEndToEndTests.cs # Testes E2E de Providers ├── Integration/ │ ├── ModuleIntegrationTests.cs # Integração entre módulos -│ └── CatalogsModuleIntegrationTests.cs +│ └── ServiceCatalogsModuleIntegrationTests.cs └── Infrastructure/ └── InfrastructureHealthTests.cs # Testes de saúde da infra ``` @@ -263,7 +263,7 @@ public class MeuTeste : TestContainerTestBase - WebApplicationFactory - Testes de infraestrutura - Testes de Users -- Testes de Catalogs +- Testes de ServiceCatalogs ### 🔄 Próximos Passos diff --git a/src/Bootstrapper/MeAjudaAi.ApiService/MeAjudaAi.ApiService.csproj b/src/Bootstrapper/MeAjudaAi.ApiService/MeAjudaAi.ApiService.csproj index 257c44fd2..ee5864d2d 100644 --- a/src/Bootstrapper/MeAjudaAi.ApiService/MeAjudaAi.ApiService.csproj +++ b/src/Bootstrapper/MeAjudaAi.ApiService/MeAjudaAi.ApiService.csproj @@ -17,11 +17,11 @@ - + diff --git a/src/Bootstrapper/MeAjudaAi.ApiService/Program.cs b/src/Bootstrapper/MeAjudaAi.ApiService/Program.cs index eab8b69dc..0bbbd5f1b 100644 --- a/src/Bootstrapper/MeAjudaAi.ApiService/Program.cs +++ b/src/Bootstrapper/MeAjudaAi.ApiService/Program.cs @@ -1,10 +1,10 @@ using System.Diagnostics; using MeAjudaAi.ApiService.Extensions; -using MeAjudaAi.Modules.Catalogs.API; using MeAjudaAi.Modules.Documents.API; using MeAjudaAi.Modules.Locations.Infrastructure; using MeAjudaAi.Modules.Providers.API; using MeAjudaAi.Modules.SearchProviders.API; +using MeAjudaAi.Modules.ServiceCatalogs.API; using MeAjudaAi.Modules.Users.API; using MeAjudaAi.ServiceDefaults; using MeAjudaAi.Shared.Extensions; @@ -34,7 +34,7 @@ public static async Task Main(string[] args) builder.Services.AddDocumentsModule(builder.Configuration); builder.Services.AddSearchProvidersModule(builder.Configuration); builder.Services.AddLocationModule(builder.Configuration); - builder.Services.AddCatalogsModule(builder.Configuration); + builder.Services.AddServiceCatalogsModule(builder.Configuration); var app = builder.Build(); @@ -108,7 +108,7 @@ private static async Task ConfigureMiddlewareAsync(WebApplication app) app.UseDocumentsModule(); app.UseSearchProvidersModule(); app.UseLocationModule(); - app.UseCatalogsModule(); + app.UseServiceCatalogsModule(); } private static void LogStartupComplete(WebApplication app) diff --git a/src/Modules/ServiceCatalogs/API/Endpoints/Service/ValidateServicesEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/Service/ValidateServicesEndpoint.cs index 671419d99..b64338f4d 100644 --- a/src/Modules/ServiceCatalogs/API/Endpoints/Service/ValidateServicesEndpoint.cs +++ b/src/Modules/ServiceCatalogs/API/Endpoints/Service/ValidateServicesEndpoint.cs @@ -1,9 +1,7 @@ using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs.Requests.Service; -using MeAjudaAi.Modules.ServiceCatalogs.Application.ModuleApi; using MeAjudaAi.Shared.Authorization; using MeAjudaAi.Shared.Contracts; -// TODO Phase 2: Uncomment when shared contracts are added -// using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs; +using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs; using MeAjudaAi.Shared.Endpoints; using MeAjudaAi.Shared.Functional; using Microsoft.AspNetCore.Builder; @@ -24,7 +22,7 @@ public static void Map(IEndpointRouteBuilder app) private static async Task ValidateAsync( [FromBody] ValidateServicesRequest request, - [FromServices] ServiceCatalogsModuleApi moduleApi, + [FromServices] IServiceCatalogsModuleApi moduleApi, CancellationToken cancellationToken) { var result = await moduleApi.ValidateServicesAsync(request.ServiceIds, cancellationToken); diff --git a/src/Modules/ServiceCatalogs/Application/Extensions.cs b/src/Modules/ServiceCatalogs/Application/Extensions.cs index 5ea0abe68..fd8aa60e7 100644 --- a/src/Modules/ServiceCatalogs/Application/Extensions.cs +++ b/src/Modules/ServiceCatalogs/Application/Extensions.cs @@ -1,6 +1,5 @@ using MeAjudaAi.Modules.ServiceCatalogs.Application.ModuleApi; -// TODO Phase 2: Uncomment when shared contracts are added -// using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs; +using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs; using Microsoft.Extensions.DependencyInjection; namespace MeAjudaAi.Modules.ServiceCatalogs.Application; @@ -9,12 +8,11 @@ public static class Extensions { public static IServiceCollection AddApplication(this IServiceCollection services) { - // Note: Handlers are automatically registered through reflection in Infrastructure layer - // via AddApplicationHandlers() which scans the Application assembly + // Note: Handlers are explicitly registered in Infrastructure layer + // via AddServiceCatalogsInfrastructure() extension method - // Module API - register concrete type for DI (interface registration in Phase 2) - // TODO Phase 2: Uncomment interface registration when IServiceCatalogsModuleApi is added - // services.AddScoped(); + // Module API - register both interface and concrete type for DI flexibility + services.AddScoped(); services.AddScoped(); return services; diff --git a/src/Modules/ServiceCatalogs/Application/ModuleApi/ServiceCatalogsModuleApi.cs b/src/Modules/ServiceCatalogs/Application/ModuleApi/ServiceCatalogsModuleApi.cs index 162ab05c2..86e43d088 100644 --- a/src/Modules/ServiceCatalogs/Application/ModuleApi/ServiceCatalogsModuleApi.cs +++ b/src/Modules/ServiceCatalogs/Application/ModuleApi/ServiceCatalogsModuleApi.cs @@ -4,9 +4,8 @@ using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; using MeAjudaAi.Shared.Constants; using MeAjudaAi.Shared.Contracts.Modules; -// TODO Phase 2: Uncomment when shared contracts are added -// using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs; -// using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs.DTOs; +using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs; +using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs.DTOs; using MeAjudaAi.Shared.Functional; using MeAjudaAi.Shared.Queries; using Microsoft.Extensions.DependencyInjection; @@ -17,13 +16,12 @@ namespace MeAjudaAi.Modules.ServiceCatalogs.Application.ModuleApi; /// /// Implementação da API pública para o módulo ServiceCatalogs. -/// TODO Phase 2: Uncomment IServiceCatalogsModuleApi interface when shared contracts are added /// [ModuleApi(ModuleMetadata.Name, ModuleMetadata.Version)] public sealed class ServiceCatalogsModuleApi( IServiceCategoryRepository categoryRepository, IServiceRepository serviceRepository, - ILogger logger) // : IServiceCatalogsModuleApi + ILogger logger) : IServiceCatalogsModuleApi { private static class ModuleMetadata { @@ -138,8 +136,7 @@ public async Task>> GetAllService categoryName, service.Name, service.Description, - service.IsActive, - service.DisplayOrder + service.IsActive ); return Result.Success(dto); @@ -161,8 +158,8 @@ public async Task>> GetAllServicesAsy var dtos = services.Select(s => new ModuleServiceListDto( s.Id.Value, + s.CategoryId.Value, s.Name, - s.Description, s.IsActive )).ToList(); @@ -194,8 +191,7 @@ public async Task>> GetServicesByCategory s.Category?.Name ?? ValidationMessages.Catalogs.UnknownCategoryName, s.Name, s.Description, - s.IsActive, - s.DisplayOrder + s.IsActive )).ToList(); return Result>.Success(dtos); @@ -293,8 +289,8 @@ public async Task> ValidateServicesAsyn var result = new ModuleServiceValidationResultDto( allValid, - [.. invalidIds], - [.. inactiveIds] + invalidIds.ToArray(), + inactiveIds.ToArray() ); return Result.Success(result); diff --git a/src/Modules/ServiceCatalogs/Application/ModuleApi/TemporaryModuleDtos.cs b/src/Modules/ServiceCatalogs/Application/ModuleApi/TemporaryModuleDtos.cs deleted file mode 100644 index 952357d84..000000000 --- a/src/Modules/ServiceCatalogs/Application/ModuleApi/TemporaryModuleDtos.cs +++ /dev/null @@ -1,47 +0,0 @@ -// TODO Phase 2: Remove this file when proper shared contracts are added in MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs.DTOs -// These are temporary placeholders to allow Phase 1b to compile without breaking the module API pattern - -namespace MeAjudaAi.Modules.ServiceCatalogs.Application.ModuleApi; - -/// -/// Temporary DTO - will be replaced by shared contract in Phase 2 -/// -public sealed record ModuleServiceCategoryDto( - Guid Id, - string Name, - string? Description, - bool IsActive, - int DisplayOrder -); - -/// -/// Temporary DTO - will be replaced by shared contract in Phase 2 -/// -public sealed record ModuleServiceDto( - Guid Id, - Guid CategoryId, - string CategoryName, - string Name, - string? Description, - bool IsActive, - int DisplayOrder -); - -/// -/// Temporary DTO - will be replaced by shared contract in Phase 2 -/// -public sealed record ModuleServiceListDto( - Guid Id, - string Name, - string? Description, - bool IsActive -); - -/// -/// Temporary DTO - will be replaced by shared contract in Phase 2 -/// -public sealed record ModuleServiceValidationResultDto( - bool AllValid, - IReadOnlyCollection InvalidServiceIds, - IReadOnlyCollection InactiveServiceIds -); diff --git a/src/Modules/ServiceCatalogs/Domain/Exceptions/CatalogDomainException.cs b/src/Modules/ServiceCatalogs/Domain/Exceptions/CatalogDomainException.cs index e3344e336..30f7a2579 100644 --- a/src/Modules/ServiceCatalogs/Domain/Exceptions/CatalogDomainException.cs +++ b/src/Modules/ServiceCatalogs/Domain/Exceptions/CatalogDomainException.cs @@ -6,7 +6,7 @@ namespace MeAjudaAi.Modules.ServiceCatalogs.Domain.Exceptions; public sealed class CatalogDomainException : Exception { public CatalogDomainException() { } - + public CatalogDomainException(string message) : base(message) { } public CatalogDomainException(string message, Exception innerException) diff --git a/src/Modules/ServiceCatalogs/Infrastructure/Extensions.cs b/src/Modules/ServiceCatalogs/Infrastructure/Extensions.cs index 487acd809..4baa86920 100644 --- a/src/Modules/ServiceCatalogs/Infrastructure/Extensions.cs +++ b/src/Modules/ServiceCatalogs/Infrastructure/Extensions.cs @@ -1,7 +1,19 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Commands.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Queries.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Queries.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.ServiceCategory; using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; using MeAjudaAi.Modules.ServiceCatalogs.Infrastructure.Persistence; using MeAjudaAi.Modules.ServiceCatalogs.Infrastructure.Persistence.Repositories; +using MeAjudaAi.Shared.Commands; using MeAjudaAi.Shared.Database; +using MeAjudaAi.Shared.Functional; +using MeAjudaAi.Shared.Queries; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -25,7 +37,6 @@ public static IServiceCollection AddServiceCatalogsInfrastructure( var isTestEnvironment = environment?.EnvironmentName == "Testing"; // Use shared connection string resolution logic (same precedence as DapperConnection) - // Try PostgresOptions first (bound from Postgres:ConnectionString) var connectionString = configuration["Postgres:ConnectionString"] ?? configuration.GetConnectionString("DefaultConnection") ?? configuration.GetConnectionString("ServiceCatalogs") @@ -48,7 +59,7 @@ public static IServiceCollection AddServiceCatalogsInfrastructure( options.UseNpgsql(connectionString, npgsqlOptions => { npgsqlOptions.MigrationsAssembly(typeof(ServiceCatalogsDbContext).Assembly.FullName); - npgsqlOptions.MigrationsHistoryTable("__EFMigrationsHistory", "ServiceCatalogs"); + npgsqlOptions.MigrationsHistoryTable("__EFMigrationsHistory", "service_catalogs"); npgsqlOptions.CommandTimeout(60); }) .UseSnakeCaseNamingConvention() @@ -56,12 +67,30 @@ public static IServiceCollection AddServiceCatalogsInfrastructure( .EnableSensitiveDataLogging(false); }); - // Register repositories // Register repositories services.AddScoped(); services.AddScoped(); - // Note: Command and Query handlers will be registered in Phase 1b when Application layer is added + // Register command handlers + services.AddScoped>, CreateServiceCategoryCommandHandler>(); + services.AddScoped>, CreateServiceCommandHandler>(); + services.AddScoped, UpdateServiceCategoryCommandHandler>(); + services.AddScoped, UpdateServiceCommandHandler>(); + services.AddScoped, DeleteServiceCategoryCommandHandler>(); + services.AddScoped, DeleteServiceCommandHandler>(); + services.AddScoped, ActivateServiceCategoryCommandHandler>(); + services.AddScoped, ActivateServiceCommandHandler>(); + services.AddScoped, DeactivateServiceCategoryCommandHandler>(); + services.AddScoped, DeactivateServiceCommandHandler>(); + services.AddScoped, ChangeServiceCategoryCommandHandler>(); + + // Register query handlers + services.AddScoped>>, GetAllServiceCategoriesQueryHandler>(); + services.AddScoped>, GetServiceCategoryByIdQueryHandler>(); + services.AddScoped>>, GetServiceCategoriesWithCountQueryHandler>(); + services.AddScoped>>, GetAllServicesQueryHandler>(); + services.AddScoped>, GetServiceByIdQueryHandler>(); + services.AddScoped>>, GetServicesByCategoryQueryHandler>(); return services; } diff --git a/src/Modules/ServiceCatalogs/Infrastructure/MeAjudaAi.Modules.ServiceCatalogs.Infrastructure.csproj b/src/Modules/ServiceCatalogs/Infrastructure/MeAjudaAi.Modules.ServiceCatalogs.Infrastructure.csproj index 4a95cdfa6..2748100af 100644 --- a/src/Modules/ServiceCatalogs/Infrastructure/MeAjudaAi.Modules.ServiceCatalogs.Infrastructure.csproj +++ b/src/Modules/ServiceCatalogs/Infrastructure/MeAjudaAi.Modules.ServiceCatalogs.Infrastructure.csproj @@ -22,6 +22,7 @@ + diff --git a/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Migrations/20251117205349_InitialCreate.Designer.cs b/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Migrations/20251117205349_InitialCreate.Designer.cs index 55cc65ba3..506c04556 100644 --- a/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Migrations/20251117205349_InitialCreate.Designer.cs +++ b/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Migrations/20251117205349_InitialCreate.Designer.cs @@ -20,7 +20,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasDefaultSchema("ServiceCatalogs") + .HasDefaultSchema("service_catalogs") .HasAnnotation("ProductVersion", "9.0.9") .HasAnnotation("Relational:MaxIdentifierLength", 63); diff --git a/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Migrations/20251117205349_InitialCreate.cs b/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Migrations/20251117205349_InitialCreate.cs index fb9bee89d..dda97ae41 100644 --- a/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Migrations/20251117205349_InitialCreate.cs +++ b/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Migrations/20251117205349_InitialCreate.cs @@ -12,11 +12,11 @@ public partial class InitialCreate : Migration protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.EnsureSchema( - name: "ServiceCatalogs"); + name: "service_catalogs"); migrationBuilder.CreateTable( name: "service_categories", - schema: "ServiceCatalogs", + schema: "service_catalogs", columns: table => new { id = table.Column(type: "uuid", nullable: false), @@ -34,7 +34,7 @@ protected override void Up(MigrationBuilder migrationBuilder) migrationBuilder.CreateTable( name: "services", - schema: "ServiceCatalogs", + schema: "service_catalogs", columns: table => new { id = table.Column(type: "uuid", nullable: false), @@ -52,7 +52,7 @@ protected override void Up(MigrationBuilder migrationBuilder) table.ForeignKey( name: "fk_services_category", column: x => x.category_id, - principalSchema: "ServiceCatalogs", + principalSchema: "service_catalogs", principalTable: "service_categories", principalColumn: "id", onDelete: ReferentialAction.Restrict); @@ -60,44 +60,44 @@ protected override void Up(MigrationBuilder migrationBuilder) migrationBuilder.CreateIndex( name: "ix_service_categories_display_order", - schema: "ServiceCatalogs", + schema: "service_catalogs", table: "service_categories", column: "display_order"); migrationBuilder.CreateIndex( name: "ix_service_categories_is_active", - schema: "ServiceCatalogs", + schema: "service_catalogs", table: "service_categories", column: "is_active"); migrationBuilder.CreateIndex( name: "ix_service_categories_name", - schema: "ServiceCatalogs", + schema: "service_catalogs", table: "service_categories", column: "name", unique: true); migrationBuilder.CreateIndex( name: "ix_services_category_display_order", - schema: "ServiceCatalogs", + schema: "service_catalogs", table: "services", columns: new[] { "category_id", "display_order" }); migrationBuilder.CreateIndex( name: "ix_services_category_id", - schema: "ServiceCatalogs", + schema: "service_catalogs", table: "services", column: "category_id"); migrationBuilder.CreateIndex( name: "ix_services_is_active", - schema: "ServiceCatalogs", + schema: "service_catalogs", table: "services", column: "is_active"); migrationBuilder.CreateIndex( name: "ix_services_name", - schema: "ServiceCatalogs", + schema: "service_catalogs", table: "services", column: "name", unique: true); @@ -108,11 +108,11 @@ protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( name: "services", - schema: "ServiceCatalogs"); + schema: "service_catalogs"); migrationBuilder.DropTable( name: "service_categories", - schema: "ServiceCatalogs"); + schema: "service_catalogs"); } } } diff --git a/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Migrations/20251119000000_RenameSchemaToSnakeCase.Designer.cs b/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Migrations/20251119000000_RenameSchemaToSnakeCase.Designer.cs new file mode 100644 index 000000000..b2b3d66d4 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Migrations/20251119000000_RenameSchemaToSnakeCase.Designer.cs @@ -0,0 +1,144 @@ +// +using System; +using MeAjudaAi.Modules.ServiceCatalogs.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.ServiceCatalogs.Infrastructure.Migrations +{ + [DbContext(typeof(ServiceCatalogsDbContext))] + [Migration("20251119000000_RenameSchemaToSnakeCase")] + partial class RenameSchemaToSnakeCase + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("service_catalogs") + .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities.Service", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CategoryId") + .HasColumnType("uuid") + .HasColumnName("category_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("DisplayOrder") + .HasColumnType("integer") + .HasColumnName("display_order"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_services_category_id"); + + b.HasIndex("IsActive") + .HasDatabaseName("ix_services_is_active"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_services_name"); + + b.HasIndex("CategoryId", "DisplayOrder") + .HasDatabaseName("ix_services_category_display_order"); + + b.ToTable("services", "service_catalogs"); + }); + + modelBuilder.Entity("MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities.ServiceCategory", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("DisplayOrder") + .HasColumnType("integer") + .HasColumnName("display_order"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasColumnName("is_active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("DisplayOrder") + .HasDatabaseName("ix_service_categories_display_order"); + + b.HasIndex("IsActive") + .HasDatabaseName("ix_service_categories_is_active"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_service_categories_name"); + + b.ToTable("service_categories", "service_catalogs"); + }); + + modelBuilder.Entity("MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities.Service", b => + { + b.HasOne("MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities.ServiceCategory", null) + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_services_category"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Migrations/20251119000000_RenameSchemaToSnakeCase.cs b/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Migrations/20251119000000_RenameSchemaToSnakeCase.cs new file mode 100644 index 000000000..3ab56ad2a --- /dev/null +++ b/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Migrations/20251119000000_RenameSchemaToSnakeCase.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MeAjudaAi.Modules.ServiceCatalogs.Infrastructure.Migrations +{ + /// + public partial class RenameSchemaToSnakeCase : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Check if old PascalCase schema exists and rename it to snake_case + // This handles databases created with the old schema name + migrationBuilder.Sql(@" + DO $$ + BEGIN + -- Check if the old schema exists + IF EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'ServiceCatalogs') THEN + -- Check if the new schema doesn't exist yet + IF NOT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'service_catalogs') THEN + -- Rename the schema + ALTER SCHEMA ""ServiceCatalogs"" RENAME TO ""service_catalogs""; + ELSE + -- Both schemas exist - this is an error state + RAISE EXCEPTION 'Both ServiceCatalogs and service_catalogs schemas exist. Manual intervention required.'; + END IF; + END IF; + -- If old schema doesn't exist, do nothing (schema was created correctly from the start) + END $$; + "); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // Rename back to PascalCase if rolling back + migrationBuilder.Sql(@" + DO $$ + BEGIN + IF EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'service_catalogs') THEN + IF NOT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'ServiceCatalogs') THEN + ALTER SCHEMA ""service_catalogs"" RENAME TO ""ServiceCatalogs""; + END IF; + END IF; + END $$; + "); + } + } +} diff --git a/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Migrations/CatalogsDbContextModelSnapshot.cs b/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Migrations/CatalogsDbContextModelSnapshot.cs index 8ef2362bf..7225169eb 100644 --- a/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Migrations/CatalogsDbContextModelSnapshot.cs +++ b/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Migrations/CatalogsDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasDefaultSchema("ServiceCatalogs") + .HasDefaultSchema("service_catalogs") .HasAnnotation("ProductVersion", "9.0.9") .HasAnnotation("Relational:MaxIdentifierLength", 63); diff --git a/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Repositories/ServiceRepository.cs b/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Repositories/ServiceRepository.cs index dbd0b2fae..06e058781 100644 --- a/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Repositories/ServiceRepository.cs +++ b/src/Modules/ServiceCatalogs/Infrastructure/Persistence/Repositories/ServiceRepository.cs @@ -17,14 +17,14 @@ public sealed class ServiceRepository(ServiceCatalogsDbContext context) : IServi public async Task> GetByIdsAsync(IEnumerable ids, CancellationToken cancellationToken = default) { - // Guard against null or empty to avoid NullReferenceException and unnecessary DB calls + // Guard against null or empty to prevent NullReferenceException if (ids == null) return Array.Empty(); - + var idList = ids.ToList(); if (idList.Count == 0) return Array.Empty(); - + return await context.Services .AsNoTracking() .Include(s => s.Category) diff --git a/src/Modules/ServiceCatalogs/Infrastructure/Persistence/ServiceCatalogsDbContext.cs b/src/Modules/ServiceCatalogs/Infrastructure/Persistence/ServiceCatalogsDbContext.cs index 1b0d2ccb3..9d160fcc1 100644 --- a/src/Modules/ServiceCatalogs/Infrastructure/Persistence/ServiceCatalogsDbContext.cs +++ b/src/Modules/ServiceCatalogs/Infrastructure/Persistence/ServiceCatalogsDbContext.cs @@ -14,7 +14,7 @@ public class ServiceCatalogsDbContext(DbContextOptions protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.HasDefaultSchema("ServiceCatalogs"); + modelBuilder.HasDefaultSchema("service_catalogs"); // Apply configurations from assembly modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); diff --git a/src/Modules/ServiceCatalogs/Infrastructure/Persistence/ServiceCatalogsDbContextFactory.cs b/src/Modules/ServiceCatalogs/Infrastructure/Persistence/ServiceCatalogsDbContextFactory.cs index c8f6861dc..80cc4e882 100644 --- a/src/Modules/ServiceCatalogs/Infrastructure/Persistence/ServiceCatalogsDbContextFactory.cs +++ b/src/Modules/ServiceCatalogs/Infrastructure/Persistence/ServiceCatalogsDbContextFactory.cs @@ -47,7 +47,7 @@ public ServiceCatalogsDbContext CreateDbContext(string[] args) connectionString, npgsqlOptions => { - npgsqlOptions.MigrationsHistoryTable("__EFMigrationsHistory", "ServiceCatalogs"); + npgsqlOptions.MigrationsHistoryTable("__EFMigrationsHistory", "service_catalogs"); npgsqlOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); }); diff --git a/src/Modules/ServiceCatalogs/Tests/Builders/ServiceBuilder.cs b/src/Modules/ServiceCatalogs/Tests/Builders/ServiceBuilder.cs new file mode 100644 index 000000000..b686b9939 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Builders/ServiceBuilder.cs @@ -0,0 +1,81 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Tests.Builders; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Builders; + +public class ServiceBuilder : BuilderBase +{ + private ServiceCategoryId? _categoryId; + private string? _name; + private string? _description; + private bool _isActive = true; + private int? _displayOrder = null; + + public ServiceBuilder() + { + Faker = new Faker() + .CustomInstantiator(f => + { + var generatedName = f.Commerce.ProductName(); + var generatedDescription = f.Commerce.ProductDescription(); + + var service = Service.Create( + _categoryId ?? ServiceCategoryId.New(), + _name ?? (generatedName.Length <= 150 ? generatedName : generatedName[..150]), + _description ?? (generatedDescription.Length <= 1000 ? generatedDescription : generatedDescription[..1000]), + _displayOrder ?? f.Random.Int(0, 100) + ); + + // Define o estado de ativo/inativo + if (!_isActive) + { + service.Deactivate(); + } + + return service; + }); + } + + public ServiceBuilder WithCategoryId(ServiceCategoryId categoryId) + { + _categoryId = categoryId; + return this; + } + + public ServiceBuilder WithCategoryId(Guid categoryId) + { + _categoryId = new ServiceCategoryId(categoryId); + return this; + } + + public ServiceBuilder WithName(string name) + { + _name = name; + return this; + } + + public ServiceBuilder WithDescription(string? description) + { + _description = description; + return this; + } + + public ServiceBuilder WithDisplayOrder(int displayOrder) + { + _displayOrder = displayOrder; + return this; + } + + public ServiceBuilder AsActive() + { + _isActive = true; + return this; + } + + public ServiceBuilder AsInactive() + { + _isActive = false; + return this; + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Builders/ServiceCategoryBuilder.cs b/src/Modules/ServiceCatalogs/Tests/Builders/ServiceCategoryBuilder.cs new file mode 100644 index 000000000..57a1ab5db --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Builders/ServiceCategoryBuilder.cs @@ -0,0 +1,66 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Shared.Tests.Builders; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Builders; + +public class ServiceCategoryBuilder : BuilderBase +{ + private string? _name; + private string? _description; + private bool _isActive = true; + private int? _displayOrder; + + public ServiceCategoryBuilder() + { + Faker = new Faker() + .CustomInstantiator(f => + { + var category = ServiceCategory.Create( + _name ?? f.Commerce.Department(), + _description ?? f.Lorem.Sentence(), + _displayOrder ?? f.Random.Int(1, 100) + ); + + // Define o estado de ativo/inativo + if (!_isActive) + { + category.Deactivate(); + } + + return category; + }); + } + + public ServiceCategoryBuilder WithName(string name) + { + _name = name; + return this; + } + + public ServiceCategoryBuilder WithDescription(string description) + { + _description = description; + return this; + } + + public ServiceCategoryBuilder WithDisplayOrder(int? displayOrder) + { + _displayOrder = displayOrder; + return this; + } + + public ServiceCategoryBuilder AsActive() + { + _isActive = true; + // CustomInstantiator will ensure category is created active + return this; + } + + public ServiceCategoryBuilder AsInactive() + { + _isActive = false; + // CustomInstantiator will call Deactivate() after creation + return this; + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/GlobalTestConfiguration.cs b/src/Modules/ServiceCatalogs/Tests/GlobalTestConfiguration.cs new file mode 100644 index 000000000..c277004ec --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/GlobalTestConfiguration.cs @@ -0,0 +1,12 @@ +using MeAjudaAi.Shared.Tests; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests; + +/// +/// Collection definition específica para testes de integração do módulo ServiceCatalogs +/// +[CollectionDefinition("ServiceCatalogsIntegrationTests")] +public class ServiceCatalogsIntegrationTestCollection : ICollectionFixture +{ + // Esta classe não tem implementação - apenas define a collection específica do módulo ServiceCatalogs +} diff --git a/src/Modules/ServiceCatalogs/Tests/Infrastructure/ServiceCatalogsIntegrationTestBase.cs b/src/Modules/ServiceCatalogs/Tests/Infrastructure/ServiceCatalogsIntegrationTestBase.cs new file mode 100644 index 000000000..bbae6a355 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Infrastructure/ServiceCatalogsIntegrationTestBase.cs @@ -0,0 +1,109 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.ServiceCatalogs.Infrastructure.Persistence; +using MeAjudaAi.Shared.Tests.Infrastructure; +using MeAjudaAi.Shared.Time; +using Microsoft.Extensions.DependencyInjection; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Infrastructure; + +/// +/// Classe base para testes de integração específicos do módulo ServiceCatalogs. +/// +public abstract class ServiceCatalogsIntegrationTestBase : IntegrationTestBase +{ + /// + /// Configurações padrão para testes do módulo ServiceCatalogs + /// + protected override TestInfrastructureOptions GetTestOptions() + { + return new TestInfrastructureOptions + { + Database = new TestDatabaseOptions + { + DatabaseName = $"test_db_{GetType().Name.ToUpperInvariant()[..Math.Min(50, GetType().Name.Length)]}", + Username = "test_user", + Password = "test_password", + Schema = "ServiceCatalogs" + }, + Cache = new TestCacheOptions + { + Enabled = true // Usa o Redis compartilhado + }, + ExternalServices = new TestExternalServicesOptions + { + UseKeycloakMock = true, + UseMessageBusMock = true + } + }; + } + + /// + /// Configura serviços específicos do módulo ServiceCatalogs + /// + protected override void ConfigureModuleServices(IServiceCollection services, TestInfrastructureOptions options) + { + services.AddServiceCatalogsTestInfrastructure(options); + } + + /// + /// Setup específico do módulo ServiceCatalogs (configurações adicionais se necessário) + /// + protected override async Task OnModuleInitializeAsync(IServiceProvider serviceProvider) + { + // Qualquer setup específico adicional do módulo ServiceCatalogs pode ser feito aqui + // As migrações são aplicadas automaticamente pelo sistema de auto-descoberta + await Task.CompletedTask; + } + + /// + /// Cria uma categoria de serviço para teste e persiste no banco de dados + /// + protected async Task CreateServiceCategoryAsync( + string name, + string? description = null, + int displayOrder = 0, + CancellationToken cancellationToken = default) + { + var category = ServiceCategory.Create(name, description, displayOrder); + + var dbContext = GetService(); + await dbContext.ServiceCategories.AddAsync(category, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); + + return category; + } + + /// + /// Cria um serviço para teste e persiste no banco de dados + /// + protected async Task CreateServiceAsync( + ServiceCategoryId categoryId, + string name, + string? description = null, + int displayOrder = 0, + CancellationToken cancellationToken = default) + { + var service = Service.Create(categoryId, name, description, displayOrder); + + var dbContext = GetService(); + await dbContext.Services.AddAsync(service, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); + + return service; + } + + /// + /// Cria uma categoria de serviço e um serviço associado + /// + protected async Task<(ServiceCategory Category, Service Service)> CreateCategoryWithServiceAsync( + string categoryName, + string serviceName, + CancellationToken cancellationToken = default) + { + var category = await CreateServiceCategoryAsync(categoryName, cancellationToken: cancellationToken); + var service = await CreateServiceAsync(category.Id, serviceName, cancellationToken: cancellationToken); + + return (category, service); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Infrastructure/TestInfrastructureExtensions.cs b/src/Modules/ServiceCatalogs/Tests/Infrastructure/TestInfrastructureExtensions.cs new file mode 100644 index 000000000..f720e3093 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Infrastructure/TestInfrastructureExtensions.cs @@ -0,0 +1,90 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Infrastructure.Persistence; +using MeAjudaAi.Modules.ServiceCatalogs.Infrastructure.Persistence.Repositories; +using MeAjudaAi.Shared.Tests.Extensions; +using MeAjudaAi.Shared.Tests.Infrastructure; +using MeAjudaAi.Shared.Time; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Testcontainers.PostgreSql; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Infrastructure; + +public static class TestInfrastructureExtensions +{ + /// + /// Adiciona infraestrutura de teste específica do módulo ServiceCatalogs + /// + public static IServiceCollection AddServiceCatalogsTestInfrastructure( + this IServiceCollection services, + TestInfrastructureOptions? options = null) + { + options ??= new TestInfrastructureOptions(); + + // Initialize nested options to ensure non-null properties + options.Database ??= new TestDatabaseOptions(); + options.Cache ??= new TestCacheOptions(); + options.ExternalServices ??= new TestExternalServicesOptions(); + + // Set default schema if not provided + if (string.IsNullOrEmpty(options.Database.Schema)) + { + options.Database.Schema = "public"; + } + + services.AddSingleton(options); + + // Adicionar serviços compartilhados essenciais + services.AddSingleton(); + + // Usar extensões compartilhadas + services.AddTestLogging(); + services.AddTestCache(options.Cache); + + // Adicionar serviços de cache do Shared + services.AddSingleton(); + + // Configurar banco de dados específico do módulo ServiceCatalogs + services.AddDbContext((serviceProvider, dbOptions) => + { + var container = serviceProvider.GetRequiredService(); + var connectionString = container.GetConnectionString(); + + dbOptions.UseNpgsql(connectionString, npgsqlOptions => + { + npgsqlOptions.MigrationsAssembly(typeof(ServiceCatalogsDbContext).Assembly.FullName); + npgsqlOptions.MigrationsHistoryTable("__EFMigrationsHistory", options.Database.Schema); + npgsqlOptions.CommandTimeout(60); + }) + .UseSnakeCaseNamingConvention() + .ConfigureWarnings(warnings => + { + warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning); + }); + }); + + // Configurar mocks específicos do módulo ServiceCatalogs + if (options.ExternalServices.UseMessageBusMock) + { + services.AddTestMessageBus(); + } + + // Adicionar repositórios específicos do ServiceCatalogs + services.AddScoped(); + services.AddScoped(); + + // Adicionar serviços de aplicação (incluindo IServiceCatalogsModuleApi) + services.AddApplication(); + + return services; + } +} + +/// +/// Implementação de IDateTimeProvider para testes +/// +internal class TestDateTimeProvider : IDateTimeProvider +{ + public DateTime CurrentDate() => DateTime.UtcNow; +} diff --git a/src/Modules/ServiceCatalogs/Tests/Integration/ServiceCatalogsModuleApiIntegrationTests.cs b/src/Modules/ServiceCatalogs/Tests/Integration/ServiceCatalogsModuleApiIntegrationTests.cs new file mode 100644 index 000000000..26df1fbfe --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Integration/ServiceCatalogsModuleApiIntegrationTests.cs @@ -0,0 +1,224 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Tests.Infrastructure; +using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs; +using MeAjudaAi.Shared.Time; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Integration; + +[Collection("ServiceCatalogsIntegrationTests")] +public class ServiceCatalogsModuleApiIntegrationTests : ServiceCatalogsIntegrationTestBase +{ + private IServiceCatalogsModuleApi _moduleApi = null!; + + protected override async Task OnModuleInitializeAsync(IServiceProvider serviceProvider) + { + await base.OnModuleInitializeAsync(serviceProvider); + _moduleApi = GetService(); + } + + [Fact] + public async Task GetServiceCategoryByIdAsync_WithExistingCategory_ShouldReturnCategory() + { + // Arrange + var category = await CreateServiceCategoryAsync("Test Category", "Test Description", 1); + + // Act + var result = await _moduleApi.GetServiceCategoryByIdAsync(category.Id.Value); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeNull(); + result.Value!.Id.Should().Be(category.Id.Value); + result.Value.Name.Should().Be("Test Category"); + result.Value.Description.Should().Be("Test Description"); + result.Value.IsActive.Should().BeTrue(); + } + + [Fact] + public async Task GetServiceCategoryByIdAsync_WithNonExistentCategory_ShouldReturnNull() + { + // Arrange + var nonExistentId = UuidGenerator.NewId(); + + // Act + var result = await _moduleApi.GetServiceCategoryByIdAsync(nonExistentId); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeNull(); + } + + [Fact] + public async Task GetAllServiceCategoriesAsync_ShouldReturnAllCategories() + { + // Arrange + await CreateServiceCategoryAsync("Category 1"); + await CreateServiceCategoryAsync("Category 2"); + await CreateServiceCategoryAsync("Category 3"); + + // Act + var result = await _moduleApi.GetAllServiceCategoriesAsync(); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().HaveCountGreaterThanOrEqualTo(3); + } + + [Fact] + public async Task GetAllServiceCategoriesAsync_WithActiveOnlyFilter_ShouldReturnOnlyActiveCategories() + { + // Arrange + var activeCategory = await CreateServiceCategoryAsync("Active Category"); + var inactiveCategory = await CreateServiceCategoryAsync("Inactive Category"); + + inactiveCategory.Deactivate(); + var repository = GetService(); + await repository.UpdateAsync(inactiveCategory); + + // Act + var result = await _moduleApi.GetAllServiceCategoriesAsync(activeOnly: true); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().Contain(c => c.Id == activeCategory.Id.Value); + result.Value.Should().NotContain(c => c.Id == inactiveCategory.Id.Value); + } + + [Fact] + public async Task GetServiceByIdAsync_WithExistingService_ShouldReturnService() + { + // Arrange + var (category, service) = await CreateCategoryWithServiceAsync("Category", "Test Service"); + + // Act + var result = await _moduleApi.GetServiceByIdAsync(service.Id.Value); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeNull(); + result.Value!.Id.Should().Be(service.Id.Value); + result.Value.Name.Should().Be("Test Service"); + result.Value.CategoryId.Should().Be(category.Id.Value); + } + + [Fact] + public async Task GetServiceByIdAsync_WithNonExistentService_ShouldReturnNull() + { + // Arrange + var nonExistentId = UuidGenerator.NewId(); + + // Act + var result = await _moduleApi.GetServiceByIdAsync(nonExistentId); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeNull(); + } + + [Fact] + public async Task GetAllServicesAsync_ShouldReturnAllServices() + { + // Arrange + var category = await CreateServiceCategoryAsync("Category"); + await CreateServiceAsync(category.Id, "Service 1"); + await CreateServiceAsync(category.Id, "Service 2"); + await CreateServiceAsync(category.Id, "Service 3"); + + // Act + var result = await _moduleApi.GetAllServicesAsync(); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().HaveCountGreaterThanOrEqualTo(3); + } + + [Fact] + public async Task GetServicesByCategoryAsync_ShouldReturnCategoryServices() + { + // Arrange + var category1 = await CreateServiceCategoryAsync("Category 1"); + var category2 = await CreateServiceCategoryAsync("Category 2"); + + var service1 = await CreateServiceAsync(category1.Id, "Service 1-1"); + var service2 = await CreateServiceAsync(category1.Id, "Service 1-2"); + await CreateServiceAsync(category2.Id, "Service 2-1"); + + // Act + var result = await _moduleApi.GetServicesByCategoryAsync(category1.Id.Value); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().HaveCount(2); + result.Value.Should().Contain(s => s.Id == service1.Id.Value); + result.Value.Should().Contain(s => s.Id == service2.Id.Value); + } + + [Fact] + public async Task IsServiceActiveAsync_WithActiveService_ShouldReturnTrue() + { + // Arrange + var category = await CreateServiceCategoryAsync("Category"); + var service = await CreateServiceAsync(category.Id, "Active Service"); + + // Act + var result = await _moduleApi.IsServiceActiveAsync(service.Id.Value); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeTrue(); + } + + [Fact] + public async Task IsServiceActiveAsync_WithInactiveService_ShouldReturnFalse() + { + // Arrange + var category = await CreateServiceCategoryAsync("Category"); + var service = await CreateServiceAsync(category.Id, "Inactive Service"); + + service.Deactivate(); + var repository = GetService(); + await repository.UpdateAsync(service); + + // Act + var result = await _moduleApi.IsServiceActiveAsync(service.Id.Value); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeFalse(); + } + + [Fact] + public async Task ValidateServicesAsync_WithAllValidServices_ShouldReturnAllValid() + { + // Arrange + var category = await CreateServiceCategoryAsync("Category"); + var service1 = await CreateServiceAsync(category.Id, "Service 1"); + var service2 = await CreateServiceAsync(category.Id, "Service 2"); + + // Act + var result = await _moduleApi.ValidateServicesAsync(new[] { service1.Id.Value, service2.Id.Value }); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.AllValid.Should().BeTrue(); + result.Value.InvalidServiceIds.Should().BeEmpty(); + result.Value.InactiveServiceIds.Should().BeEmpty(); + } + + [Fact] + public async Task ValidateServicesAsync_WithSomeInvalidServices_ShouldReturnMixedResult() + { + // Arrange + var category = await CreateServiceCategoryAsync("Category"); + var validService = await CreateServiceAsync(category.Id, "Valid Service"); + var invalidServiceId = UuidGenerator.NewId(); + + // Act + var result = await _moduleApi.ValidateServicesAsync(new[] { validService.Id.Value, invalidServiceId }); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.AllValid.Should().BeFalse(); + result.Value.InvalidServiceIds.Should().HaveCount(1); + result.Value.InvalidServiceIds.Should().Contain(invalidServiceId); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Integration/ServiceCategoryRepositoryIntegrationTests.cs b/src/Modules/ServiceCatalogs/Tests/Integration/ServiceCategoryRepositoryIntegrationTests.cs new file mode 100644 index 000000000..9f1a40135 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Integration/ServiceCategoryRepositoryIntegrationTests.cs @@ -0,0 +1,156 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.ServiceCatalogs.Tests.Infrastructure; +using MeAjudaAi.Shared.Time; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Integration; + +[Collection("ServiceCatalogsIntegrationTests")] +public class ServiceCategoryRepositoryIntegrationTests : ServiceCatalogsIntegrationTestBase +{ + private IServiceCategoryRepository _repository = null!; + + protected override async Task OnModuleInitializeAsync(IServiceProvider serviceProvider) + { + await base.OnModuleInitializeAsync(serviceProvider); + _repository = GetService(); + } + + [Fact] + public async Task GetByIdAsync_WithExistingCategory_ShouldReturnCategory() + { + // Arrange + var category = await CreateServiceCategoryAsync("Test Category", "Test Description", 1); + + // Act + var result = await _repository.GetByIdAsync(category.Id); + + // Assert + result.Should().NotBeNull(); + result!.Id.Should().Be(category.Id); + result.Name.Should().Be("Test Category"); + result.Description.Should().Be("Test Description"); + result.DisplayOrder.Should().Be(1); + result.IsActive.Should().BeTrue(); + } + + [Fact] + public async Task GetByIdAsync_WithNonExistentCategory_ShouldReturnNull() + { + // Arrange + var nonExistentId = UuidGenerator.NewId(); + + // Act + var result = await _repository.GetByIdAsync(ServiceCategoryId.From(nonExistentId)); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public async Task GetAllAsync_WithMultipleCategories_ShouldReturnAllCategories() + { + // Arrange + await CreateServiceCategoryAsync("Category 1", displayOrder: 1); + await CreateServiceCategoryAsync("Category 2", displayOrder: 2); + await CreateServiceCategoryAsync("Category 3", displayOrder: 3); + + // Act + var result = await _repository.GetAllAsync(); + + // Assert + result.Should().HaveCountGreaterThanOrEqualTo(3); + result.Should().Contain(c => c.Name == "Category 1"); + result.Should().Contain(c => c.Name == "Category 2"); + result.Should().Contain(c => c.Name == "Category 3"); + } + + [Fact] + public async Task GetAllAsync_WithActiveOnlyFilter_ShouldReturnOnlyActiveCategories() + { + // Arrange + var activeCategory = await CreateServiceCategoryAsync("Active Category"); + var inactiveCategory = await CreateServiceCategoryAsync("Inactive Category"); + + inactiveCategory.Deactivate(); + await _repository.UpdateAsync(inactiveCategory); + + // Act + var result = await _repository.GetAllAsync(activeOnly: true); + + // Assert + result.Should().Contain(c => c.Id == activeCategory.Id); + result.Should().NotContain(c => c.Id == inactiveCategory.Id); + } + + [Fact] + public async Task ExistsWithNameAsync_WithExistingName_ShouldReturnTrue() + { + // Arrange + await CreateServiceCategoryAsync("Unique Category"); + + // Act + var result = await _repository.ExistsWithNameAsync("Unique Category"); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public async Task ExistsWithNameAsync_WithNonExistentName_ShouldReturnFalse() + { + // Act + var result = await _repository.ExistsWithNameAsync("Non Existent Category"); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public async Task AddAsync_WithValidCategory_ShouldPersistCategory() + { + // Arrange + var category = ServiceCategory.Create("New Category", "New Description", 10); + + // Act + await _repository.AddAsync(category); + + // Assert + var retrievedCategory = await _repository.GetByIdAsync(category.Id); + retrievedCategory.Should().NotBeNull(); + retrievedCategory!.Name.Should().Be("New Category"); + } + + [Fact] + public async Task UpdateAsync_WithModifiedCategory_ShouldPersistChanges() + { + // Arrange + var category = await CreateServiceCategoryAsync("Original Name"); + + // Act + category.Update("Updated Name", "Updated Description", 5); + await _repository.UpdateAsync(category); + + // Assert + var retrievedCategory = await _repository.GetByIdAsync(category.Id); + retrievedCategory.Should().NotBeNull(); + retrievedCategory!.Name.Should().Be("Updated Name"); + retrievedCategory.Description.Should().Be("Updated Description"); + retrievedCategory.DisplayOrder.Should().Be(5); + } + + [Fact] + public async Task DeleteAsync_WithExistingCategory_ShouldRemoveCategory() + { + // Arrange + var category = await CreateServiceCategoryAsync("To Be Deleted"); + + // Act + await _repository.DeleteAsync(category.Id); + + // Assert + var retrievedCategory = await _repository.GetByIdAsync(category.Id); + retrievedCategory.Should().BeNull(); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Integration/ServiceRepositoryIntegrationTests.cs b/src/Modules/ServiceCatalogs/Tests/Integration/ServiceRepositoryIntegrationTests.cs new file mode 100644 index 000000000..672d1be23 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Integration/ServiceRepositoryIntegrationTests.cs @@ -0,0 +1,200 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.ServiceCatalogs.Tests.Builders; +using MeAjudaAi.Modules.ServiceCatalogs.Tests.Infrastructure; +using MeAjudaAi.Shared.Time; +using Domain = MeAjudaAi.Modules.ServiceCatalogs.Domain; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Integration; + +[Collection("ServiceCatalogsIntegrationTests")] +public class ServiceRepositoryIntegrationTests : ServiceCatalogsIntegrationTestBase +{ + private IServiceRepository _repository = null!; + + protected override async Task OnModuleInitializeAsync(IServiceProvider serviceProvider) + { + await base.OnModuleInitializeAsync(serviceProvider); + _repository = GetService(); + } + + [Fact] + public async Task GetByIdAsync_WithExistingService_ShouldReturnService() + { + // Arrange + var (category, service) = await CreateCategoryWithServiceAsync("Test Category", "Test Service"); + + // Act + var result = await _repository.GetByIdAsync(service.Id); + + // Assert + result.Should().NotBeNull(); + result!.Id.Should().Be(service.Id); + result.Name.Should().Be("Test Service"); + result.CategoryId.Should().Be(category.Id); + } + + [Fact] + public async Task GetByIdAsync_WithNonExistentService_ShouldReturnNull() + { + // Arrange + var nonExistentId = UuidGenerator.NewId(); + + // Act + var result = await _repository.GetByIdAsync(new Domain.ValueObjects.ServiceId(nonExistentId)); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public async Task GetAllAsync_WithMultipleServices_ShouldReturnAllServices() + { + // Arrange + var category = await CreateServiceCategoryAsync("Category"); + await CreateServiceAsync(category.Id, "Service 1", displayOrder: 1); + await CreateServiceAsync(category.Id, "Service 2", displayOrder: 2); + await CreateServiceAsync(category.Id, "Service 3", displayOrder: 3); + + // Act + var result = await _repository.GetAllAsync(); + + // Assert + result.Should().HaveCountGreaterThanOrEqualTo(3); + result.Should().Contain(s => s.Name == "Service 1"); + result.Should().Contain(s => s.Name == "Service 2"); + result.Should().Contain(s => s.Name == "Service 3"); + } + + [Fact] + public async Task GetAllAsync_WithActiveOnlyFilter_ShouldReturnOnlyActiveServices() + { + // Arrange + var category = await CreateServiceCategoryAsync("Category"); + var activeService = await CreateServiceAsync(category.Id, "Active Service"); + var inactiveService = await CreateServiceAsync(category.Id, "Inactive Service"); + + inactiveService.Deactivate(); + await _repository.UpdateAsync(inactiveService); + + // Act + var result = await _repository.GetAllAsync(activeOnly: true); + + // Assert + result.Should().Contain(s => s.Id == activeService.Id); + result.Should().OnlyContain(s => s.IsActive, "all returned services should be active"); + result.Should().NotContain(s => s.Id == inactiveService.Id); + } + + [Fact] + public async Task GetByCategoryAsync_WithExistingCategory_ShouldReturnCategoryServices() + { + // Arrange + var category1 = await CreateServiceCategoryAsync("Category 1"); + var category2 = await CreateServiceCategoryAsync("Category 2"); + + var service1 = await CreateServiceAsync(category1.Id, "Service 1-1"); + var service2 = await CreateServiceAsync(category1.Id, "Service 1-2"); + await CreateServiceAsync(category2.Id, "Service 2-1"); + + // Act + var result = await _repository.GetByCategoryAsync(category1.Id); + + // Assert + result.Should().HaveCount(2); + result.Should().Contain(s => s.Id == service1.Id); + result.Should().Contain(s => s.Id == service2.Id); + } + + [Fact] + public async Task ExistsWithNameAsync_WithExistingName_ShouldReturnTrue() + { + // Arrange + var category = await CreateServiceCategoryAsync("Category"); + await CreateServiceAsync(category.Id, "Unique Service"); + + // Act + var result = await _repository.ExistsWithNameAsync("Unique Service"); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public async Task ExistsWithNameAsync_WithNonExistentName_ShouldReturnFalse() + { + // Act + var result = await _repository.ExistsWithNameAsync("Non Existent Service"); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public async Task AddAsync_WithValidService_ShouldPersistService() + { + // Arrange + var category = await CreateServiceCategoryAsync("Category"); + var service = Domain.Entities.Service.Create(category.Id, "New Service", "New Description", 10); + + // Act + await _repository.AddAsync(service); + + // Assert + var retrievedService = await _repository.GetByIdAsync(service.Id); + retrievedService.Should().NotBeNull(); + retrievedService!.Name.Should().Be("New Service"); + } + + [Fact] + public async Task UpdateAsync_WithModifiedService_ShouldPersistChanges() + { + // Arrange + var category = await CreateServiceCategoryAsync("Category"); + var service = await CreateServiceAsync(category.Id, "Original Name"); + + // Act + service.Update("Updated Name", "Updated Description", 5); + await _repository.UpdateAsync(service); + + // Assert + var retrievedService = await _repository.GetByIdAsync(service.Id); + retrievedService.Should().NotBeNull(); + retrievedService!.Name.Should().Be("Updated Name"); + retrievedService.Description.Should().Be("Updated Description"); + retrievedService.DisplayOrder.Should().Be(5); + } + + [Fact] + public async Task DeleteAsync_WithExistingService_ShouldRemoveService() + { + // Arrange + var category = await CreateServiceCategoryAsync("Category"); + var service = await CreateServiceAsync(category.Id, "To Be Deleted"); + + // Act + await _repository.DeleteAsync(service.Id); + + // Assert + var retrievedService = await _repository.GetByIdAsync(service.Id); + retrievedService.Should().BeNull(); + } + + [Fact] + public async Task ChangeCategory_WithDifferentCategory_ShouldUpdateCategoryReference() + { + // Arrange + var category1 = await CreateServiceCategoryAsync("Category 1"); + var category2 = await CreateServiceCategoryAsync("Category 2"); + var service = await CreateServiceAsync(category1.Id, "Test Service"); + + // Act + service.ChangeCategory(category2.Id); + await _repository.UpdateAsync(service); + + // Assert + var retrievedService = await _repository.GetByIdAsync(service.Id); + retrievedService.Should().NotBeNull(); + retrievedService!.CategoryId.Should().Be(category2.Id); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/MeAjudaAi.Modules.ServiceCatalogs.Tests.csproj b/src/Modules/ServiceCatalogs/Tests/MeAjudaAi.Modules.ServiceCatalogs.Tests.csproj new file mode 100644 index 000000000..cefc275c2 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/MeAjudaAi.Modules.ServiceCatalogs.Tests.csproj @@ -0,0 +1,56 @@ + + + + net9.0 + enable + enable + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/ActivateServiceCategoryCommandHandlerTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/ActivateServiceCategoryCommandHandlerTests.cs new file mode 100644 index 000000000..a58d8c031 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/ActivateServiceCategoryCommandHandlerTests.cs @@ -0,0 +1,114 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Commands.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.ServiceCatalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Application.Handlers.Commands; + +[Trait("Category", "Unit")] +[Trait("Module", "ServiceCatalogs")] +[Trait("Layer", "Application")] +public class ActivateServiceCategoryCommandHandlerTests +{ + private readonly Mock _repositoryMock; + private readonly ActivateServiceCategoryCommandHandler _handler; + + public ActivateServiceCategoryCommandHandlerTests() + { + _repositoryMock = new Mock(); + _handler = new ActivateServiceCategoryCommandHandler(_repositoryMock.Object); + } + + [Fact] + public async Task Handle_WithValidCommand_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder() + .WithName("Limpeza") + .AsInactive() + .Build(); + var command = new ActivateServiceCategoryCommand(category.Id.Value); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(category); + + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + category.IsActive.Should().BeTrue(); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Once); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNonExistentCategory_ShouldReturnFailure() + { + // Arrange + var categoryId = Guid.NewGuid(); + var command = new ActivateServiceCategoryCommand(categoryId); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((ServiceCategory?)null); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("not found"); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithEmptyId_ShouldReturnFailure() + { + // Arrange + var command = new ActivateServiceCategoryCommand(Guid.Empty); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("cannot be empty"); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Never); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithAlreadyActiveCategory_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder() + .WithName("Limpeza") + .AsActive() + .Build(); + var command = new ActivateServiceCategoryCommand(category.Id.Value); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(category); + + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + category.IsActive.Should().BeTrue(); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/ActivateServiceCommandHandlerTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/ActivateServiceCommandHandlerTests.cs new file mode 100644 index 000000000..42235cbad --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/ActivateServiceCommandHandlerTests.cs @@ -0,0 +1,118 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.ServiceCatalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Application.Handlers.Commands; + +[Trait("Category", "Unit")] +[Trait("Module", "ServiceCatalogs")] +[Trait("Layer", "Application")] +public class ActivateServiceCommandHandlerTests +{ + private readonly Mock _repositoryMock; + private readonly ActivateServiceCommandHandler _handler; + + public ActivateServiceCommandHandlerTests() + { + _repositoryMock = new Mock(); + _handler = new ActivateServiceCommandHandler(_repositoryMock.Object); + } + + [Fact] + public async Task Handle_WithValidCommand_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(category.Id) + .WithName("Limpeza de Piscina") + .AsInactive() + .Build(); + var command = new ActivateServiceCommand(service.Id.Value); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + service.IsActive.Should().BeTrue(); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Once); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNonExistentService_ShouldReturnFailure() + { + // Arrange + var serviceId = Guid.NewGuid(); + var command = new ActivateServiceCommand(serviceId); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((Service?)null); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("not found"); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithEmptyId_ShouldReturnFailure() + { + // Arrange + var command = new ActivateServiceCommand(Guid.Empty); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("cannot be empty"); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Never); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithAlreadyActiveService_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(category.Id) + .WithName("Limpeza de Piscina") + .AsActive() + .Build(); + var command = new ActivateServiceCommand(service.Id.Value); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + service.IsActive.Should().BeTrue(); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/ChangeServiceCategoryCommandHandlerTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/ChangeServiceCategoryCommandHandlerTests.cs new file mode 100644 index 000000000..ce2b03b8f --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/ChangeServiceCategoryCommandHandlerTests.cs @@ -0,0 +1,206 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.ServiceCatalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Application.Handlers.Commands; + +[Trait("Category", "Unit")] +[Trait("Module", "ServiceCatalogs")] +[Trait("Layer", "Application")] +public class ChangeServiceCategoryCommandHandlerTests +{ + private readonly Mock _serviceRepositoryMock; + private readonly Mock _categoryRepositoryMock; + private readonly ChangeServiceCategoryCommandHandler _handler; + + public ChangeServiceCategoryCommandHandlerTests() + { + _serviceRepositoryMock = new Mock(); + _categoryRepositoryMock = new Mock(); + _handler = new ChangeServiceCategoryCommandHandler(_serviceRepositoryMock.Object, _categoryRepositoryMock.Object); + } + + [Fact] + public async Task Handle_WithValidCommand_ShouldReturnSuccess() + { + // Arrange + var oldCategory = new ServiceCategoryBuilder().AsActive().Build(); + var newCategory = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(oldCategory.Id) + .WithName("Limpeza de Piscina") + .Build(); + var command = new ChangeServiceCategoryCommand(service.Id.Value, newCategory.Id.Value); + + _serviceRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _categoryRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(newCategory); + + _serviceRepositoryMock + .Setup(x => x.ExistsWithNameAsync(service.Name, It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(false); + + _serviceRepositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + service.CategoryId.Should().Be(newCategory.Id); + _serviceRepositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNonExistentService_ShouldReturnFailure() + { + // Arrange + var serviceId = Guid.NewGuid(); + var categoryId = Guid.NewGuid(); + var command = new ChangeServiceCategoryCommand(serviceId, categoryId); + + _serviceRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((Service?)null); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("Service").And.Contain("not found"); + _serviceRepositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithNonExistentCategory_ShouldReturnFailure() + { + // Arrange + var category = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(category.Id) + .WithName("Limpeza de Piscina") + .Build(); + var newCategoryId = Guid.NewGuid(); + var command = new ChangeServiceCategoryCommand(service.Id.Value, newCategoryId); + + _serviceRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _categoryRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((ServiceCategory?)null); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("Category").And.Contain("not found"); + _serviceRepositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithInactiveCategory_ShouldReturnFailure() + { + // Arrange + var oldCategory = new ServiceCategoryBuilder().AsActive().Build(); + var newCategory = new ServiceCategoryBuilder().AsInactive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(oldCategory.Id) + .WithName("Limpeza de Piscina") + .Build(); + var command = new ChangeServiceCategoryCommand(service.Id.Value, newCategory.Id.Value); + + _serviceRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _categoryRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(newCategory); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("inactive"); + _serviceRepositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithDuplicateNameInTargetCategory_ShouldReturnFailure() + { + // Arrange + var oldCategory = new ServiceCategoryBuilder().AsActive().Build(); + var newCategory = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(oldCategory.Id) + .WithName("Limpeza de Piscina") + .Build(); + var command = new ChangeServiceCategoryCommand(service.Id.Value, newCategory.Id.Value); + + _serviceRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _categoryRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(newCategory); + + _serviceRepositoryMock + .Setup(x => x.ExistsWithNameAsync(service.Name, It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("already exists"); + _serviceRepositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithEmptyServiceId_ShouldReturnFailure() + { + // Arrange + var categoryId = Guid.NewGuid(); + var command = new ChangeServiceCategoryCommand(Guid.Empty, categoryId); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("Service ID").And.Contain("cannot be empty"); + _serviceRepositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithEmptyCategoryId_ShouldReturnFailure() + { + // Arrange + var serviceId = Guid.NewGuid(); + var command = new ChangeServiceCategoryCommand(serviceId, Guid.Empty); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("category ID").And.Contain("cannot be empty"); + _serviceRepositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Never); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/CreateServiceCategoryCommandHandlerTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/CreateServiceCategoryCommandHandlerTests.cs new file mode 100644 index 000000000..34d264f61 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/CreateServiceCategoryCommandHandlerTests.cs @@ -0,0 +1,89 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Commands.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Application.Handlers.Commands; + +[Trait("Category", "Unit")] +[Trait("Module", "ServiceCatalogs")] +[Trait("Layer", "Application")] +public class CreateServiceCategoryCommandHandlerTests +{ + private readonly Mock _repositoryMock; + private readonly CreateServiceCategoryCommandHandler _handler; + + public CreateServiceCategoryCommandHandlerTests() + { + _repositoryMock = new Mock(); + _handler = new CreateServiceCategoryCommandHandler(_repositoryMock.Object); + } + + [Fact] + public async Task Handle_WithValidCommand_ShouldReturnSuccess() + { + // Arrange + var command = new CreateServiceCategoryCommand("Limpeza", "Serviços de limpeza", 1); + + _repositoryMock + .Setup(x => x.ExistsWithNameAsync(command.Name, null, It.IsAny())) + .ReturnsAsync(false); + + _repositoryMock + .Setup(x => x.AddAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeNull(); + result.Value.Id.Should().NotBe(Guid.Empty); + + _repositoryMock.Verify(x => x.ExistsWithNameAsync(command.Name, null, It.IsAny()), Times.Once); + _repositoryMock.Verify(x => x.AddAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithDuplicateName_ShouldReturnFailure() + { + // Arrange + var command = new CreateServiceCategoryCommand("Limpeza", "Serviços de limpeza", 1); + + _repositoryMock + .Setup(x => x.ExistsWithNameAsync(command.Name, null, It.IsAny())) + .ReturnsAsync(true); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.IsSuccess.Should().BeFalse(); + result.Error.Should().NotBeNull(); + result.Error!.Message.Should().Contain("already exists"); + + _repositoryMock.Verify(x => x.ExistsWithNameAsync(command.Name, null, It.IsAny()), Times.Once); + _repositoryMock.Verify(x => x.AddAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public async Task Handle_WithInvalidName_ShouldReturnFailure(string? invalidName) + { + // Arrange + var command = new CreateServiceCategoryCommand(invalidName!, "Description", 1); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error.Should().NotBeNull(); + result.Error!.Message.Should().Contain("name", "validation error should mention the problematic field"); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/CreateServiceCommandHandlerTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/CreateServiceCommandHandlerTests.cs new file mode 100644 index 000000000..69dbb3dc2 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/CreateServiceCommandHandlerTests.cs @@ -0,0 +1,120 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.ServiceCatalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Application.Handlers.Commands; + +[Trait("Category", "Unit")] +[Trait("Module", "ServiceCatalogs")] +[Trait("Layer", "Application")] +public class CreateServiceCommandHandlerTests +{ + private readonly Mock _categoryRepositoryMock; + private readonly Mock _serviceRepositoryMock; + private readonly CreateServiceCommandHandler _handler; + + public CreateServiceCommandHandlerTests() + { + _categoryRepositoryMock = new Mock(); + _serviceRepositoryMock = new Mock(); + _handler = new CreateServiceCommandHandler(_serviceRepositoryMock.Object, _categoryRepositoryMock.Object); + } + + [Fact] + public async Task Handle_WithValidCommand_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder().AsActive().Build(); + var command = new CreateServiceCommand(category.Id.Value, "Limpeza de Piscina", "Limpeza profunda", 1); + + _categoryRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(category); + + _serviceRepositoryMock + .Setup(x => x.ExistsWithNameAsync(command.Name, null, It.IsAny(), It.IsAny())) + .ReturnsAsync(false); + + _serviceRepositoryMock + .Setup(x => x.AddAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeNull(); + result.Value.Id.Should().NotBe(Guid.Empty); + result.Value.Name.Should().Be(command.Name); + result.Value.CategoryId.Should().Be(command.CategoryId); + _serviceRepositoryMock.Verify(x => x.AddAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNonExistentCategory_ShouldReturnFailure() + { + // Arrange + var categoryId = Guid.NewGuid(); + var command = new CreateServiceCommand(categoryId, "Service Name", "Description", 1); + + _categoryRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((ServiceCategory?)null); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("not found"); + _serviceRepositoryMock.Verify(x => x.AddAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithInactiveCategory_ShouldReturnFailure() + { + // Arrange + var category = new ServiceCategoryBuilder().AsInactive().Build(); + var command = new CreateServiceCommand(category.Id.Value, "Limpeza de Piscina", "Limpeza profunda", 1); + + _categoryRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(category); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("inactive"); + _serviceRepositoryMock.Verify(x => x.AddAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithDuplicateName_ShouldReturnFailure() + { + // Arrange + var category = new ServiceCategoryBuilder().AsActive().Build(); + var command = new CreateServiceCommand(category.Id.Value, "Duplicate Name", "Description", 1); + + _categoryRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(category); + + _serviceRepositoryMock + .Setup(x => x.ExistsWithNameAsync(command.Name, null, It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("already exists"); + _serviceRepositoryMock.Verify(x => x.AddAsync(It.IsAny(), It.IsAny()), Times.Never); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/DeactivateServiceCategoryCommandHandlerTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/DeactivateServiceCategoryCommandHandlerTests.cs new file mode 100644 index 000000000..5b31040b1 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/DeactivateServiceCategoryCommandHandlerTests.cs @@ -0,0 +1,114 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Commands.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.ServiceCatalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Application.Handlers.Commands; + +[Trait("Category", "Unit")] +[Trait("Module", "ServiceCatalogs")] +[Trait("Layer", "Application")] +public class DeactivateServiceCategoryCommandHandlerTests +{ + private readonly Mock _repositoryMock; + private readonly DeactivateServiceCategoryCommandHandler _handler; + + public DeactivateServiceCategoryCommandHandlerTests() + { + _repositoryMock = new Mock(); + _handler = new DeactivateServiceCategoryCommandHandler(_repositoryMock.Object); + } + + [Fact] + public async Task Handle_WithValidCommand_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder() + .WithName("Limpeza") + .AsActive() + .Build(); + var command = new DeactivateServiceCategoryCommand(category.Id.Value); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(category); + + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + category.IsActive.Should().BeFalse(); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Once); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNonExistentCategory_ShouldReturnFailure() + { + // Arrange + var categoryId = Guid.NewGuid(); + var command = new DeactivateServiceCategoryCommand(categoryId); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((ServiceCategory?)null); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("not found"); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithEmptyId_ShouldReturnFailure() + { + // Arrange + var command = new DeactivateServiceCategoryCommand(Guid.Empty); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("cannot be empty"); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Never); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithAlreadyInactiveCategory_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder() + .WithName("Limpeza") + .AsInactive() + .Build(); + var command = new DeactivateServiceCategoryCommand(category.Id.Value); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(category); + + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + category.IsActive.Should().BeFalse(); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/DeactivateServiceCommandHandlerTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/DeactivateServiceCommandHandlerTests.cs new file mode 100644 index 000000000..a847851d0 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/DeactivateServiceCommandHandlerTests.cs @@ -0,0 +1,118 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.ServiceCatalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Application.Handlers.Commands; + +[Trait("Category", "Unit")] +[Trait("Module", "ServiceCatalogs")] +[Trait("Layer", "Application")] +public class DeactivateServiceCommandHandlerTests +{ + private readonly Mock _repositoryMock; + private readonly DeactivateServiceCommandHandler _handler; + + public DeactivateServiceCommandHandlerTests() + { + _repositoryMock = new Mock(); + _handler = new DeactivateServiceCommandHandler(_repositoryMock.Object); + } + + [Fact] + public async Task Handle_WithValidCommand_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(category.Id) + .WithName("Limpeza de Piscina") + .AsActive() + .Build(); + var command = new DeactivateServiceCommand(service.Id.Value); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + service.IsActive.Should().BeFalse(); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Once); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNonExistentService_ShouldReturnFailure() + { + // Arrange + var serviceId = Guid.NewGuid(); + var command = new DeactivateServiceCommand(serviceId); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((Service?)null); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("not found"); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithEmptyId_ShouldReturnFailure() + { + // Arrange + var command = new DeactivateServiceCommand(Guid.Empty); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("cannot be empty"); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Never); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithAlreadyInactiveService_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(category.Id) + .WithName("Limpeza de Piscina") + .AsInactive() + .Build(); + var command = new DeactivateServiceCommand(service.Id.Value); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + service.IsActive.Should().BeFalse(); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/DeleteServiceCategoryCommandHandlerTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/DeleteServiceCategoryCommandHandlerTests.cs new file mode 100644 index 000000000..8f04240d7 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/DeleteServiceCategoryCommandHandlerTests.cs @@ -0,0 +1,96 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Commands.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.ServiceCatalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Application.Handlers.Commands; + +[Trait("Category", "Unit")] +[Trait("Module", "ServiceCatalogs")] +[Trait("Layer", "Application")] +public class DeleteServiceCategoryCommandHandlerTests +{ + private readonly Mock _categoryRepositoryMock; + private readonly Mock _serviceRepositoryMock; + private readonly DeleteServiceCategoryCommandHandler _handler; + + public DeleteServiceCategoryCommandHandlerTests() + { + _categoryRepositoryMock = new Mock(); + _serviceRepositoryMock = new Mock(); + _handler = new DeleteServiceCategoryCommandHandler(_categoryRepositoryMock.Object, _serviceRepositoryMock.Object); + } + + [Fact] + public async Task Handle_WithValidCommand_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder().WithName("Limpeza").Build(); + var command = new DeleteServiceCategoryCommand(category.Id.Value); + + _categoryRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(category); + + _serviceRepositoryMock + .Setup(x => x.CountByCategoryAsync(It.IsAny(), false, It.IsAny())) + .ReturnsAsync(0); + + _categoryRepositoryMock + .Setup(x => x.DeleteAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + _categoryRepositoryMock.Verify(x => x.DeleteAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNonExistentCategory_ShouldReturnFailure() + { + // Arrange + var categoryId = Guid.NewGuid(); + var command = new DeleteServiceCategoryCommand(categoryId); + + _categoryRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((ServiceCategory?)null); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("not found"); + _categoryRepositoryMock.Verify(x => x.DeleteAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithAssociatedServices_ShouldReturnFailure() + { + // Arrange + var category = new ServiceCategoryBuilder().WithName("Limpeza").Build(); + var command = new DeleteServiceCategoryCommand(category.Id.Value); + + _categoryRepositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(category); + + _serviceRepositoryMock + .Setup(x => x.CountByCategoryAsync(It.IsAny(), false, It.IsAny())) + .ReturnsAsync(3); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("Cannot delete"); + _categoryRepositoryMock.Verify(x => x.DeleteAsync(It.IsAny(), It.IsAny()), Times.Never); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/DeleteServiceCommandHandlerTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/DeleteServiceCommandHandlerTests.cs new file mode 100644 index 000000000..ae157d957 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/DeleteServiceCommandHandlerTests.cs @@ -0,0 +1,87 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.ServiceCatalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Application.Handlers.Commands; + +[Trait("Category", "Unit")] +[Trait("Module", "ServiceCatalogs")] +[Trait("Layer", "Application")] +public class DeleteServiceCommandHandlerTests +{ + private readonly Mock _repositoryMock; + private readonly DeleteServiceCommandHandler _handler; + + public DeleteServiceCommandHandlerTests() + { + _repositoryMock = new Mock(); + _handler = new DeleteServiceCommandHandler(_repositoryMock.Object); + } + + [Fact] + public async Task Handle_WithValidCommand_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(category.Id) + .WithName("Limpeza de Piscina") + .Build(); + var command = new DeleteServiceCommand(service.Id.Value); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _repositoryMock + .Setup(x => x.DeleteAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Once); + _repositoryMock.Verify(x => x.DeleteAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNonExistentService_ShouldReturnFailure() + { + // Arrange + var serviceId = Guid.NewGuid(); + var command = new DeleteServiceCommand(serviceId); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((Service?)null); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("not found"); + _repositoryMock.Verify(x => x.DeleteAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithEmptyId_ShouldReturnFailure() + { + // Arrange + var command = new DeleteServiceCommand(Guid.Empty); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("cannot be empty"); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Never); + _repositoryMock.Verify(x => x.DeleteAsync(It.IsAny(), It.IsAny()), Times.Never); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/UpdateServiceCategoryCommandHandlerTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/UpdateServiceCategoryCommandHandlerTests.cs new file mode 100644 index 000000000..d704a2b49 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/UpdateServiceCategoryCommandHandlerTests.cs @@ -0,0 +1,99 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Commands.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.ServiceCatalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Application.Handlers.Commands; + +[Trait("Category", "Unit")] +[Trait("Module", "ServiceCatalogs")] +[Trait("Layer", "Application")] +public class UpdateServiceCategoryCommandHandlerTests +{ + private readonly Mock _repositoryMock; + private readonly UpdateServiceCategoryCommandHandler _handler; + + public UpdateServiceCategoryCommandHandlerTests() + { + _repositoryMock = new Mock(); + _handler = new UpdateServiceCategoryCommandHandler(_repositoryMock.Object); + } + + [Fact] + public async Task Handle_WithValidCommand_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder() + .WithName("Original Name") + .Build(); + var command = new UpdateServiceCategoryCommand(category.Id.Value, "Updated Name", "Updated Description", 2); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(category); + + _repositoryMock + .Setup(x => x.ExistsWithNameAsync(command.Name, It.IsAny(), It.IsAny())) + .ReturnsAsync(false); + + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Once); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNonExistentCategory_ShouldReturnFailure() + { + // Arrange + var categoryId = Guid.NewGuid(); + var command = new UpdateServiceCategoryCommand(categoryId, "Updated Name", "Updated Description", 2); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((ServiceCategory?)null); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("not found"); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithDuplicateName_ShouldReturnFailure() + { + // Arrange + var category = new ServiceCategoryBuilder() + .WithName("Original Name") + .Build(); + var command = new UpdateServiceCategoryCommand(category.Id.Value, "Duplicate Name", "Description", 2); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(category); + + _repositoryMock + .Setup(x => x.ExistsWithNameAsync(command.Name, It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("already exists"); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/UpdateServiceCommandHandlerTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/UpdateServiceCommandHandlerTests.cs new file mode 100644 index 000000000..77147761d --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Commands/UpdateServiceCommandHandlerTests.cs @@ -0,0 +1,148 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.ServiceCatalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Application.Handlers.Commands; + +[Trait("Category", "Unit")] +[Trait("Module", "ServiceCatalogs")] +[Trait("Layer", "Application")] +public class UpdateServiceCommandHandlerTests +{ + private readonly Mock _repositoryMock; + private readonly UpdateServiceCommandHandler _handler; + + public UpdateServiceCommandHandlerTests() + { + _repositoryMock = new Mock(); + _handler = new UpdateServiceCommandHandler(_repositoryMock.Object); + } + + [Fact] + public async Task Handle_WithValidCommand_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(category.Id) + .WithName("Original Name") + .Build(); + var command = new UpdateServiceCommand(service.Id.Value, "Updated Name", "Updated Description", 2); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _repositoryMock + .Setup(x => x.ExistsWithNameAsync(command.Name, It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(false); + + _repositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + service.Name.Should().Be(command.Name); + service.Description.Should().Be(command.Description); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Once); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNonExistentService_ShouldReturnFailure() + { + // Arrange + var serviceId = Guid.NewGuid(); + var command = new UpdateServiceCommand(serviceId, "Updated Name", "Updated Description", 2); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((Service?)null); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("not found"); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithEmptyId_ShouldReturnFailure() + { + // Arrange + var command = new UpdateServiceCommand(Guid.Empty, "Name", "Description", 1); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("cannot be empty"); + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Never); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithDuplicateName_ShouldReturnFailure() + { + // Arrange + var category = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(category.Id) + .WithName("Original Name") + .Build(); + var command = new UpdateServiceCommand(service.Id.Value, "Duplicate Name", "Description", 2); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + _repositoryMock + .Setup(x => x.ExistsWithNameAsync(command.Name, It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error!.Message.Should().Contain("already exists"); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public async Task Handle_WithInvalidName_ShouldReturnFailure(string? invalidName) + { + // Arrange + var category = new ServiceCategoryBuilder().AsActive().Build(); + var service = new ServiceBuilder() + .WithCategoryId(category.Id) + .WithName("Valid Name") + .Build(); + var command = new UpdateServiceCommand(service.Id.Value, invalidName!, "Description", 1); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + // Act + var result = await _handler.HandleAsync(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error.Should().NotBeNull(); + _repositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), Times.Never); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Queries/GetAllServiceCategoriesQueryHandlerTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Queries/GetAllServiceCategoriesQueryHandlerTests.cs new file mode 100644 index 000000000..f54879e37 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Queries/GetAllServiceCategoriesQueryHandlerTests.cs @@ -0,0 +1,96 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Queries.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Application.Handlers.Queries; + +[Trait("Category", "Unit")] +[Trait("Module", "ServiceCatalogs")] +[Trait("Layer", "Application")] +public class GetAllServiceCategoriesQueryHandlerTests +{ + private readonly Mock _repositoryMock; + private readonly GetAllServiceCategoriesQueryHandler _handler; + + public GetAllServiceCategoriesQueryHandlerTests() + { + _repositoryMock = new Mock(); + _handler = new GetAllServiceCategoriesQueryHandler(_repositoryMock.Object); + } + + [Fact] + public async Task Handle_ShouldReturnAllCategories() + { + // Arrange + var query = new GetAllServiceCategoriesQuery(ActiveOnly: false); + var categories = new List + { + new ServiceCategoryBuilder().WithName("Limpeza").Build(), + new ServiceCategoryBuilder().WithName("Reparos").Build(), + new ServiceCategoryBuilder().WithName("Pintura").Build() + }; + + _repositoryMock + .Setup(x => x.GetAllAsync(false, It.IsAny())) + .ReturnsAsync(categories); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().HaveCount(3); + result.Value.Should().Contain(c => c.Name == "Limpeza"); + result.Value.Should().Contain(c => c.Name == "Reparos"); + result.Value.Should().Contain(c => c.Name == "Pintura"); + + _repositoryMock.Verify(x => x.GetAllAsync(false, It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithActiveOnlyTrue_ShouldReturnOnlyActiveCategories() + { + // Arrange + var query = new GetAllServiceCategoriesQuery(ActiveOnly: true); + var categories = new List + { + new ServiceCategoryBuilder().WithName("Limpeza").AsActive().Build(), + new ServiceCategoryBuilder().WithName("Reparos").AsActive().Build() + }; + + _repositoryMock + .Setup(x => x.GetAllAsync(true, It.IsAny())) + .ReturnsAsync(categories); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().HaveCount(2); + result.Value.Should().OnlyContain(c => c.IsActive); + + _repositoryMock.Verify(x => x.GetAllAsync(true, It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNoCategories_ShouldReturnEmptyList() + { + // Arrange + var query = new GetAllServiceCategoriesQuery(ActiveOnly: false); + + _repositoryMock + .Setup(x => x.GetAllAsync(false, It.IsAny())) + .ReturnsAsync(new List()); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeEmpty(); + + _repositoryMock.Verify(x => x.GetAllAsync(false, It.IsAny()), Times.Once); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Queries/GetAllServicesQueryHandlerTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Queries/GetAllServicesQueryHandlerTests.cs new file mode 100644 index 000000000..02a9bae29 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Queries/GetAllServicesQueryHandlerTests.cs @@ -0,0 +1,95 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Queries.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Application.Handlers.Queries; + +[Trait("Category", "Unit")] +[Trait("Module", "ServiceCatalogs")] +[Trait("Layer", "Application")] +public class GetAllServicesQueryHandlerTests +{ + private readonly Mock _repositoryMock; + private readonly GetAllServicesQueryHandler _handler; + + public GetAllServicesQueryHandlerTests() + { + _repositoryMock = new Mock(); + _handler = new GetAllServicesQueryHandler(_repositoryMock.Object); + } + + [Fact] + public async Task Handle_ShouldReturnAllServices() + { + // Arrange + var query = new GetAllServicesQuery(ActiveOnly: false); + var categoryId = Guid.NewGuid(); + var services = new List + { + new ServiceBuilder().WithCategoryId(categoryId).WithName("Service 1").Build(), + new ServiceBuilder().WithCategoryId(categoryId).WithName("Service 2").Build(), + new ServiceBuilder().WithCategoryId(categoryId).WithName("Service 3").Build() + }; + + _repositoryMock + .Setup(x => x.GetAllAsync(false, It.IsAny())) + .ReturnsAsync(services); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().HaveCount(3); + + _repositoryMock.Verify(x => x.GetAllAsync(false, It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithActiveOnlyTrue_ShouldReturnOnlyActiveServices() + { + // Arrange + var query = new GetAllServicesQuery(ActiveOnly: true); + var categoryId = Guid.NewGuid(); + var services = new List + { + new ServiceBuilder().WithCategoryId(categoryId).WithName("Active 1").AsActive().Build(), + new ServiceBuilder().WithCategoryId(categoryId).WithName("Active 2").AsActive().Build() + }; + + _repositoryMock + .Setup(x => x.GetAllAsync(true, It.IsAny())) + .ReturnsAsync(services); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().HaveCount(2); + result.Value.Should().OnlyContain(s => s.IsActive); + + _repositoryMock.Verify(x => x.GetAllAsync(true, It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNoServices_ShouldReturnEmptyList() + { + // Arrange + var query = new GetAllServicesQuery(ActiveOnly: false); + + _repositoryMock + .Setup(x => x.GetAllAsync(false, It.IsAny())) + .ReturnsAsync(new List()); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeEmpty(); + + _repositoryMock.Verify(x => x.GetAllAsync(false, It.IsAny()), Times.Once); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Queries/GetServiceByIdQueryHandlerTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Queries/GetServiceByIdQueryHandlerTests.cs new file mode 100644 index 000000000..bc96005bd --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Queries/GetServiceByIdQueryHandlerTests.cs @@ -0,0 +1,74 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Queries.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.ServiceCatalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Application.Handlers.Queries; + +[Trait("Category", "Unit")] +[Trait("Module", "ServiceCatalogs")] +[Trait("Layer", "Application")] +public class GetServiceByIdQueryHandlerTests +{ + private readonly Mock _repositoryMock; + private readonly GetServiceByIdQueryHandler _handler; + + public GetServiceByIdQueryHandlerTests() + { + _repositoryMock = new Mock(); + _handler = new GetServiceByIdQueryHandler(_repositoryMock.Object); + } + + [Fact] + public async Task Handle_WithExistingService_ShouldReturnSuccess() + { + // Arrange + var categoryId = Guid.NewGuid(); + var service = new ServiceBuilder() + .WithCategoryId(categoryId) + .WithName("Limpeza de Piscina") + .WithDescription("Limpeza profunda de piscina") + .Build(); + var query = new GetServiceByIdQuery(service.Id.Value); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(service); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeNull(); + result.Value!.Id.Should().Be(service.Id.Value); + result.Value.CategoryId.Should().Be(categoryId); + result.Value.Name.Should().Be("Limpeza de Piscina"); + result.Value.Description.Should().Be("Limpeza profunda de piscina"); + + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNonExistentService_ShouldReturnNull() + { + // Arrange + var serviceId = Guid.NewGuid(); + var query = new GetServiceByIdQuery(serviceId); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((Service?)null); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeNull(); + + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Once); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Queries/GetServiceCategoriesWithCountQueryHandlerTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Queries/GetServiceCategoriesWithCountQueryHandlerTests.cs new file mode 100644 index 000000000..35b243b90 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Queries/GetServiceCategoriesWithCountQueryHandlerTests.cs @@ -0,0 +1,164 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Queries.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.ServiceCatalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Application.Handlers.Queries; + +[Trait("Category", "Unit")] +[Trait("Module", "ServiceCatalogs")] +[Trait("Layer", "Application")] +public class GetServiceCategoriesWithCountQueryHandlerTests +{ + private readonly Mock _categoryRepositoryMock; + private readonly Mock _serviceRepositoryMock; + private readonly GetServiceCategoriesWithCountQueryHandler _handler; + + public GetServiceCategoriesWithCountQueryHandlerTests() + { + _categoryRepositoryMock = new Mock(); + _serviceRepositoryMock = new Mock(); + _handler = new GetServiceCategoriesWithCountQueryHandler(_categoryRepositoryMock.Object, _serviceRepositoryMock.Object); + } + + [Fact] + public async Task Handle_ShouldReturnCategoriesWithCounts() + { + // Arrange + var query = new GetServiceCategoriesWithCountQuery(ActiveOnly: false); + var category1 = new ServiceCategoryBuilder().WithName("Limpeza").AsActive().Build(); + var category2 = new ServiceCategoryBuilder().WithName("Reparos").AsActive().Build(); + var categories = new List + { + category1, + category2 + }; + + _categoryRepositoryMock + .Setup(x => x.GetAllAsync(false, It.IsAny())) + .ReturnsAsync(categories); + + _serviceRepositoryMock + .Setup(x => x.CountByCategoryAsync(category1.Id, false, It.IsAny())) + .ReturnsAsync(5); + + _serviceRepositoryMock + .Setup(x => x.CountByCategoryAsync(category1.Id, true, It.IsAny())) + .ReturnsAsync(3); + + _serviceRepositoryMock + .Setup(x => x.CountByCategoryAsync(category2.Id, false, It.IsAny())) + .ReturnsAsync(8); + + _serviceRepositoryMock + .Setup(x => x.CountByCategoryAsync(category2.Id, true, It.IsAny())) + .ReturnsAsync(6); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().HaveCount(2); + + var limpeza = result.Value.First(c => c.Name == "Limpeza"); + limpeza.TotalServicesCount.Should().Be(5); + limpeza.ActiveServicesCount.Should().Be(3); + + var reparos = result.Value.First(c => c.Name == "Reparos"); + reparos.TotalServicesCount.Should().Be(8); + reparos.ActiveServicesCount.Should().Be(6); + + _categoryRepositoryMock.Verify(x => x.GetAllAsync(false, It.IsAny()), Times.Once); + _serviceRepositoryMock.Verify(x => x.CountByCategoryAsync(It.IsAny(), false, It.IsAny()), Times.Exactly(2)); + _serviceRepositoryMock.Verify(x => x.CountByCategoryAsync(It.IsAny(), true, It.IsAny()), Times.Exactly(2)); + } + + [Fact] + public async Task Handle_WithActiveOnlyTrue_ShouldReturnOnlyActiveCategories() + { + // Arrange + var query = new GetServiceCategoriesWithCountQuery(ActiveOnly: true); + var category1 = new ServiceCategoryBuilder().WithName("Limpeza").AsActive().Build(); + var category2 = new ServiceCategoryBuilder().WithName("Reparos").AsActive().Build(); + var categories = new List + { + category1, + category2 + }; + + _categoryRepositoryMock + .Setup(x => x.GetAllAsync(true, It.IsAny())) + .ReturnsAsync(categories); + + _serviceRepositoryMock + .Setup(x => x.CountByCategoryAsync(It.IsAny(), false, It.IsAny())) + .ReturnsAsync(5); + + _serviceRepositoryMock + .Setup(x => x.CountByCategoryAsync(It.IsAny(), true, It.IsAny())) + .ReturnsAsync(3); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().HaveCount(2); + result.Value.Should().OnlyContain(c => c.IsActive); + + _categoryRepositoryMock.Verify(x => x.GetAllAsync(true, It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNoCategories_ShouldReturnEmptyList() + { + // Arrange + var query = new GetServiceCategoriesWithCountQuery(ActiveOnly: false); + + _categoryRepositoryMock + .Setup(x => x.GetAllAsync(false, It.IsAny())) + .ReturnsAsync(new List()); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeEmpty(); + + _categoryRepositoryMock.Verify(x => x.GetAllAsync(false, It.IsAny()), Times.Once); + _serviceRepositoryMock.Verify(x => x.CountByCategoryAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Handle_WithCategoriesWithNoServices_ShouldReturnZeroCounts() + { + // Arrange + var query = new GetServiceCategoriesWithCountQuery(ActiveOnly: false); + var category = new ServiceCategoryBuilder().WithName("Limpeza").AsActive().Build(); + var categories = new List { category }; + + _categoryRepositoryMock + .Setup(x => x.GetAllAsync(false, It.IsAny())) + .ReturnsAsync(categories); + + _serviceRepositoryMock + .Setup(x => x.CountByCategoryAsync(category.Id, false, It.IsAny())) + .ReturnsAsync(0); + + _serviceRepositoryMock + .Setup(x => x.CountByCategoryAsync(category.Id, true, It.IsAny())) + .ReturnsAsync(0); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().HaveCount(1); + result.Value.First().TotalServicesCount.Should().Be(0); + result.Value.First().ActiveServicesCount.Should().Be(0); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Queries/GetServiceCategoryByIdQueryHandlerTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Queries/GetServiceCategoryByIdQueryHandlerTests.cs new file mode 100644 index 000000000..9a6bf2b50 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Queries/GetServiceCategoryByIdQueryHandlerTests.cs @@ -0,0 +1,70 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Queries.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.ServiceCatalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Application.Handlers.Queries; + +[Trait("Category", "Unit")] +[Trait("Module", "ServiceCatalogs")] +[Trait("Layer", "Application")] +public class GetServiceCategoryByIdQueryHandlerTests +{ + private readonly Mock _repositoryMock; + private readonly GetServiceCategoryByIdQueryHandler _handler; + + public GetServiceCategoryByIdQueryHandlerTests() + { + _repositoryMock = new Mock(); + _handler = new GetServiceCategoryByIdQueryHandler(_repositoryMock.Object); + } + + [Fact] + public async Task Handle_WithExistingCategory_ShouldReturnSuccess() + { + // Arrange + var category = new ServiceCategoryBuilder() + .WithName("Limpeza") + .WithDescription("Serviços de limpeza") + .Build(); + var query = new GetServiceCategoryByIdQuery(category.Id.Value); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(category); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeNull(); + result.Value!.Id.Should().Be(category.Id.Value); + result.Value.Name.Should().Be("Limpeza"); + result.Value.Description.Should().Be("Serviços de limpeza"); + + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNonExistentCategory_ShouldReturnNull() + { + // Arrange + var categoryId = Guid.NewGuid(); + var query = new GetServiceCategoryByIdQuery(categoryId); + + _repositoryMock + .Setup(x => x.GetByIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities.ServiceCategory?)null); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeNull(); + + _repositoryMock.Verify(x => x.GetByIdAsync(It.IsAny(), It.IsAny()), Times.Once); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Queries/GetServicesByCategoryQueryHandlerTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Queries/GetServicesByCategoryQueryHandlerTests.cs new file mode 100644 index 000000000..ed6c61333 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Application/Handlers/Queries/GetServicesByCategoryQueryHandlerTests.cs @@ -0,0 +1,96 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Application.Handlers.Queries.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Repositories; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.ServiceCatalogs.Tests.Builders; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Application.Handlers.Queries; + +[Trait("Category", "Unit")] +[Trait("Module", "ServiceCatalogs")] +[Trait("Layer", "Application")] +public class GetServicesByCategoryQueryHandlerTests +{ + private readonly Mock _repositoryMock; + private readonly GetServicesByCategoryQueryHandler _handler; + + public GetServicesByCategoryQueryHandlerTests() + { + _repositoryMock = new Mock(); + _handler = new GetServicesByCategoryQueryHandler(_repositoryMock.Object); + } + + [Fact] + public async Task Handle_WithExistingCategory_ShouldReturnServices() + { + // Arrange + var categoryId = Guid.NewGuid(); + var query = new GetServicesByCategoryQuery(categoryId, ActiveOnly: false); + var services = new List + { + new ServiceBuilder().WithCategoryId(categoryId).WithName("Service 1").Build(), + new ServiceBuilder().WithCategoryId(categoryId).WithName("Service 2").Build() + }; + + _repositoryMock + .Setup(x => x.GetByCategoryAsync(It.IsAny(), false, It.IsAny())) + .ReturnsAsync(services); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().HaveCount(2); + result.Value.Should().AllSatisfy(s => s.CategoryId.Should().Be(categoryId)); + + _repositoryMock.Verify(x => x.GetByCategoryAsync(It.IsAny(), false, It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithActiveOnlyTrue_ShouldReturnOnlyActiveServices() + { + // Arrange + var categoryId = Guid.NewGuid(); + var query = new GetServicesByCategoryQuery(categoryId, ActiveOnly: true); + var services = new List + { + new ServiceBuilder().WithCategoryId(categoryId).WithName("Active").AsActive().Build() + }; + + _repositoryMock + .Setup(x => x.GetByCategoryAsync(It.IsAny(), true, It.IsAny())) + .ReturnsAsync(services); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().HaveCount(1); + result.Value.Should().OnlyContain(s => s.IsActive); + + _repositoryMock.Verify(x => x.GetByCategoryAsync(It.IsAny(), true, It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_WithNoServices_ShouldReturnEmptyList() + { + // Arrange + var categoryId = Guid.NewGuid(); + var query = new GetServicesByCategoryQuery(categoryId, ActiveOnly: false); + + _repositoryMock + .Setup(x => x.GetByCategoryAsync(It.IsAny(), false, It.IsAny())) + .ReturnsAsync(new List()); + + // Act + var result = await _handler.HandleAsync(query, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeEmpty(); + + _repositoryMock.Verify(x => x.GetByCategoryAsync(It.IsAny(), false, It.IsAny()), Times.Once); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Domain/Entities/ServiceCategoryTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Domain/Entities/ServiceCategoryTests.cs new file mode 100644 index 000000000..3de9d4fe9 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Domain/Entities/ServiceCategoryTests.cs @@ -0,0 +1,252 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Events; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Exceptions; +using MeAjudaAi.Shared.Constants; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Domain.Entities; + +public class ServiceCategoryTests +{ + [Fact] + public void Create_WithValidParameters_ShouldCreateServiceCategory() + { + // Arrange + var name = "Home Repairs"; + var description = "General home repair services"; + var displayOrder = 1; + var before = DateTime.UtcNow; + + // Act + var category = ServiceCategory.Create(name, description, displayOrder); + + // Assert + category.Should().NotBeNull(); + category.Id.Should().NotBeNull(); + category.Id.Value.Should().NotBe(Guid.Empty); + category.Name.Should().Be(name); + category.Description.Should().Be(description); + category.DisplayOrder.Should().Be(displayOrder); + category.IsActive.Should().BeTrue(); + category.CreatedAt.Should().BeOnOrAfter(before).And.BeOnOrBefore(DateTime.UtcNow); + + // Service categories are created (domain events are raised internally but not exposed publicly) + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void Create_WithInvalidName_ShouldThrowCatalogDomainException(string? invalidName) + { + // Act & Assert + var act = () => ServiceCategory.Create(invalidName!, null, 0); + act.Should().Throw() + .WithMessage("*name*"); + } + + [Fact] + public void Create_WithNameAtMaxLength_ShouldSucceed() + { + // Arrange + var maxLengthName = new string('a', ValidationConstants.CatalogLimits.ServiceCategoryNameMaxLength); + + // Act + var category = ServiceCategory.Create(maxLengthName, null, 0); + + // Assert + category.Should().NotBeNull(); + category.Name.Should().HaveLength(ValidationConstants.CatalogLimits.ServiceCategoryNameMaxLength); + } + + [Fact] + public void Create_WithNameExceedingMaxLength_ShouldThrowCatalogDomainException() + { + // Arrange + var tooLongName = new string('a', ValidationConstants.CatalogLimits.ServiceCategoryNameMaxLength + 1); + + // Act & Assert + var act = () => ServiceCategory.Create(tooLongName, null, 0); + act.Should().Throw() + .WithMessage($"*cannot exceed {ValidationConstants.CatalogLimits.ServiceCategoryNameMaxLength} characters*"); + } + + [Fact] + public void Create_WithNegativeDisplayOrder_ShouldThrowCatalogDomainException() + { + // Arrange + var name = "Test Category"; + var negativeDisplayOrder = -1; + + // Act & Assert + var act = () => ServiceCategory.Create(name, null, negativeDisplayOrder); + act.Should().Throw() + .WithMessage("*Display order cannot be negative*"); + } + + [Fact] + public void Create_WithLeadingAndTrailingSpacesInName_ShouldTrimSpaces() + { + // Arrange + var nameWithSpaces = " Test Category "; + var expectedName = "Test Category"; + + // Act + var category = ServiceCategory.Create(nameWithSpaces, null, 0); + + // Assert + category.Name.Should().Be(expectedName); + } + + [Fact] + public void Create_WithLeadingAndTrailingSpacesInDescription_ShouldTrimSpaces() + { + // Arrange + var descriptionWithSpaces = " Test Description "; + var expectedDescription = "Test Description"; + + // Act + var category = ServiceCategory.Create("Test", descriptionWithSpaces, 0); + + // Assert + category.Description.Should().Be(expectedDescription); + } + + [Fact] + public void Create_WithDescriptionAtMaxLength_ShouldSucceed() + { + // Arrange + var maxLengthDescription = new string('a', ValidationConstants.CatalogLimits.ServiceCategoryDescriptionMaxLength); + + // Act + var category = ServiceCategory.Create("Test", maxLengthDescription, 0); + + // Assert + category.Should().NotBeNull(); + category.Description.Should().HaveLength(ValidationConstants.CatalogLimits.ServiceCategoryDescriptionMaxLength); + } + + [Fact] + public void Create_WithDescriptionExceedingMaxLength_ShouldThrowCatalogDomainException() + { + // Arrange + var tooLongDescription = new string('a', ValidationConstants.CatalogLimits.ServiceCategoryDescriptionMaxLength + 1); + + // Act & Assert + var act = () => ServiceCategory.Create("Test", tooLongDescription, 0); + act.Should().Throw() + .WithMessage($"*cannot exceed {ValidationConstants.CatalogLimits.ServiceCategoryDescriptionMaxLength} characters*"); + } + + [Fact] + public void Update_WithValidParameters_ShouldUpdateServiceCategory() + { + // Arrange + var category = ServiceCategory.Create("Original Name", "Original Description", 1); + var before = DateTime.UtcNow; + + var newName = "Updated Name"; + var newDescription = "Updated Description"; + var newDisplayOrder = 2; + + // Act + category.Update(newName, newDescription, newDisplayOrder); + + // Assert + category.Name.Should().Be(newName); + category.Description.Should().Be(newDescription); + category.DisplayOrder.Should().Be(newDisplayOrder); + category.UpdatedAt.Should().NotBeNull() + .And.Subject.Should().BeOnOrAfter(before).And.BeOnOrBefore(DateTime.UtcNow); + } + + [Fact] + public void Update_WithLeadingAndTrailingSpaces_ShouldTrimSpaces() + { + // Arrange + var category = ServiceCategory.Create("Original", "Original Description", 1); + + // Act + category.Update(" Updated Name ", " Updated Description ", 2); + + // Assert + category.Name.Should().Be("Updated Name"); + category.Description.Should().Be("Updated Description"); + } + + [Fact] + public void Update_WithNegativeDisplayOrder_ShouldThrowCatalogDomainException() + { + // Arrange + var category = ServiceCategory.Create("Test Category", null, 0); + var negativeDisplayOrder = -1; + + // Act & Assert + var act = () => category.Update("Updated Name", null, negativeDisplayOrder); + act.Should().Throw() + .WithMessage("*Display order cannot be negative*"); + } + + [Fact] + public void Activate_WhenInactive_ShouldActivateCategoryAndUpdateTimestamp() + { + // Arrange + var category = ServiceCategory.Create("Test Category", null, 0); + category.Deactivate(); + var before = DateTime.UtcNow; + + // Act + category.Activate(); + + // Assert + category.IsActive.Should().BeTrue(); + category.UpdatedAt.Should().NotBeNull() + .And.Subject.Should().BeOnOrAfter(before).And.BeOnOrBefore(DateTime.UtcNow); + } + + [Fact] + public void Activate_WhenAlreadyActive_ShouldRemainActiveWithoutUpdatingTimestamp() + { + // Arrange + var category = ServiceCategory.Create("Test Category", null, 0); + var originalUpdatedAt = category.UpdatedAt; + + // Act + category.Activate(); + + // Assert + category.IsActive.Should().BeTrue(); + category.UpdatedAt.Should().Be(originalUpdatedAt); + } + + [Fact] + public void Deactivate_WhenActive_ShouldDeactivateCategoryAndUpdateTimestamp() + { + // Arrange + var category = ServiceCategory.Create("Test Category", null, 0); + var before = DateTime.UtcNow; + + // Act + category.Deactivate(); + + // Assert + category.IsActive.Should().BeFalse(); + category.UpdatedAt.Should().NotBeNull() + .And.Subject.Should().BeOnOrAfter(before).And.BeOnOrBefore(DateTime.UtcNow); + } + + [Fact] + public void Deactivate_WhenAlreadyInactive_ShouldRemainInactiveWithoutUpdatingTimestamp() + { + // Arrange + var category = ServiceCategory.Create("Test Category", null, 0); + category.Deactivate(); + var updatedAtAfterFirstDeactivate = category.UpdatedAt; + + // Act + category.Deactivate(); + + // Assert + category.IsActive.Should().BeFalse(); + category.UpdatedAt.Should().Be(updatedAtAfterFirstDeactivate); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Domain/Entities/ServiceTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Domain/Entities/ServiceTests.cs new file mode 100644 index 000000000..f8f28543d --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Domain/Entities/ServiceTests.cs @@ -0,0 +1,195 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Events; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Exceptions; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Domain.Entities; + +public class ServiceTests +{ + [Fact] + public void Create_WithValidParameters_ShouldCreateService() + { + // Arrange + var categoryId = new ServiceCategoryId(Guid.NewGuid()); + var name = "Plumbing Repair"; + var description = "Fix leaks and pipes"; + var displayOrder = 1; + + // Act + var service = Service.Create(categoryId, name, description, displayOrder); + + // Assert + service.Should().NotBeNull(); + service.Id.Should().NotBeNull(); + service.Id.Value.Should().NotBe(Guid.Empty); + service.CategoryId.Should().Be(categoryId); + service.Name.Should().Be(name); + service.Description.Should().Be(description); + service.DisplayOrder.Should().Be(displayOrder); + service.IsActive.Should().BeTrue(); + service.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1)); + + // Services are created (domain events are raised internally but not exposed publicly) + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void Create_WithInvalidName_ShouldThrowCatalogDomainException(string? invalidName) + { + // Arrange + var categoryId = new ServiceCategoryId(Guid.NewGuid()); + + // Act & Assert + var act = () => Service.Create(categoryId, invalidName!, null, 0); + act.Should().Throw() + .WithMessage("*name*"); + } + + [Fact] + public void Create_WithTooLongName_ShouldThrowCatalogDomainException() + { + // Arrange + var categoryId = new ServiceCategoryId(Guid.NewGuid()); + var longName = new string('a', 151); + + // Act & Assert + var act = () => Service.Create(categoryId, longName, null, 0); + act.Should().Throw(); + } + + [Fact] + public void Create_WithNegativeDisplayOrder_ShouldThrowCatalogDomainException() + { + // Arrange + var categoryId = new ServiceCategoryId(Guid.NewGuid()); + + // Act & Assert + var act = () => Service.Create(categoryId, "Valid Name", null, -1); + act.Should().Throw() + .WithMessage("*Display order cannot be negative*"); + } + + [Fact] + public void Update_WithValidParameters_ShouldUpdateService() + { + // Arrange + var categoryId = new ServiceCategoryId(Guid.NewGuid()); + var service = Service.Create(categoryId, "Original Name", "Original Description", 1); + + var newName = "Updated Name"; + var newDescription = "Updated Description"; + var newDisplayOrder = 2; + + // Act + service.Update(newName, newDescription, newDisplayOrder); + + // Assert + service.Name.Should().Be(newName); + service.Description.Should().Be(newDescription); + service.DisplayOrder.Should().Be(newDisplayOrder); + service.UpdatedAt.Should().NotBeNull(); + } + + [Fact] + public void Update_WithNegativeDisplayOrder_ShouldThrowCatalogDomainException() + { + // Arrange + var categoryId = new ServiceCategoryId(Guid.NewGuid()); + var service = Service.Create(categoryId, "Test Service", null, 0); + + // Act & Assert + var act = () => service.Update("Valid Name", null, -5); + act.Should().Throw() + .WithMessage("*Display order cannot be negative*"); + } + + [Fact] + public void ChangeCategory_WithDifferentCategory_ShouldChangeCategory() + { + // Arrange + var originalCategoryId = new ServiceCategoryId(Guid.NewGuid()); + var newCategoryId = new ServiceCategoryId(Guid.NewGuid()); + var service = Service.Create(originalCategoryId, "Test Service", null, 0); + + // Act + service.ChangeCategory(newCategoryId); + + // Assert + service.CategoryId.Should().Be(newCategoryId); + } + + [Fact] + public void ChangeCategory_WithSameCategory_ShouldNotChange() + { + // Arrange + var categoryId = new ServiceCategoryId(Guid.NewGuid()); + var service = Service.Create(categoryId, "Test Service", null, 0); + + // Act + service.ChangeCategory(categoryId); + + // Assert + service.CategoryId.Should().Be(categoryId); + } + + [Fact] + public void Activate_WhenInactive_ShouldActivateService() + { + // Arrange + var categoryId = new ServiceCategoryId(Guid.NewGuid()); + var service = Service.Create(categoryId, "Test Service", null, 0); + service.Deactivate(); + + // Act + service.Activate(); + + // Assert + service.IsActive.Should().BeTrue(); + } + + [Fact] + public void Activate_WhenAlreadyActive_ShouldRemainActive() + { + // Arrange + var categoryId = new ServiceCategoryId(Guid.NewGuid()); + var service = Service.Create(categoryId, "Test Service", null, 0); + + // Act + service.Activate(); + + // Assert + service.IsActive.Should().BeTrue(); + } + + [Fact] + public void Deactivate_WhenActive_ShouldDeactivateService() + { + // Arrange + var categoryId = new ServiceCategoryId(Guid.NewGuid()); + var service = Service.Create(categoryId, "Test Service", null, 0); + + // Act + service.Deactivate(); + + // Assert + service.IsActive.Should().BeFalse(); + } + + [Fact] + public void Deactivate_WhenAlreadyInactive_ShouldRemainInactive() + { + // Arrange + var categoryId = new ServiceCategoryId(Guid.NewGuid()); + var service = Service.Create(categoryId, "Test Service", null, 0); + service.Deactivate(); + + // Act + service.Deactivate(); + + // Assert + service.IsActive.Should().BeFalse(); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Domain/ValueObjects/ServiceCategoryIdTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Domain/ValueObjects/ServiceCategoryIdTests.cs new file mode 100644 index 000000000..0707e6b43 --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Domain/ValueObjects/ServiceCategoryIdTests.cs @@ -0,0 +1,86 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Domain.ValueObjects; + +public class ServiceCategoryIdTests +{ + [Fact] + public void Constructor_WithValidGuid_ShouldCreateServiceCategoryId() + { + // Arrange + var guid = Guid.NewGuid(); + + // Act + var categoryId = new ServiceCategoryId(guid); + + // Assert + categoryId.Value.Should().Be(guid); + } + + [Fact] + public void Constructor_WithEmptyGuid_ShouldThrowArgumentException() + { + // Arrange + var emptyGuid = Guid.Empty; + + // Act & Assert + var act = () => new ServiceCategoryId(emptyGuid); + act.Should().Throw() + .WithMessage("ServiceCategoryId cannot be empty*"); + } + + [Fact] + public void Equals_WithSameValue_ShouldReturnTrue() + { + // Arrange + var guid = Guid.NewGuid(); + var categoryId1 = new ServiceCategoryId(guid); + var categoryId2 = new ServiceCategoryId(guid); + + // Act & Assert + categoryId1.Should().Be(categoryId2); + categoryId1.GetHashCode().Should().Be(categoryId2.GetHashCode()); + } + + [Fact] + public void Equals_WithDifferentValues_ShouldReturnFalse() + { + // Arrange + var categoryId1 = new ServiceCategoryId(Guid.NewGuid()); + var categoryId2 = new ServiceCategoryId(Guid.NewGuid()); + + // Act & Assert + categoryId1.Should().NotBe(categoryId2); + } + + [Fact] + public void ToString_ShouldReturnGuidString() + { + // Arrange + var guid = Guid.NewGuid(); + var categoryId = new ServiceCategoryId(guid); + + // Act + var result = categoryId.ToString(); + + // Assert + result.Should().Be(guid.ToString()); + } + + [Fact] + public void ValueObject_Equality_ShouldWorkCorrectly() + { + // Arrange + var guid = Guid.NewGuid(); + var categoryId1 = new ServiceCategoryId(guid); + var categoryId2 = new ServiceCategoryId(guid); + var categoryId3 = new ServiceCategoryId(Guid.NewGuid()); + + // Act & Assert + (categoryId1 == categoryId2).Should().BeTrue(); + (categoryId1 != categoryId3).Should().BeTrue(); + categoryId1.Equals(categoryId2).Should().BeTrue(); + categoryId1.Equals(categoryId3).Should().BeFalse(); + categoryId1.Equals(null).Should().BeFalse(); + } +} diff --git a/src/Modules/ServiceCatalogs/Tests/Unit/Domain/ValueObjects/ServiceIdTests.cs b/src/Modules/ServiceCatalogs/Tests/Unit/Domain/ValueObjects/ServiceIdTests.cs new file mode 100644 index 000000000..0e1ccdaef --- /dev/null +++ b/src/Modules/ServiceCatalogs/Tests/Unit/Domain/ValueObjects/ServiceIdTests.cs @@ -0,0 +1,86 @@ +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; + +namespace MeAjudaAi.Modules.ServiceCatalogs.Tests.Unit.Domain.ValueObjects; + +public class ServiceIdTests +{ + [Fact] + public void Constructor_WithValidGuid_ShouldCreateServiceId() + { + // Arrange + var guid = Guid.NewGuid(); + + // Act + var serviceId = new ServiceId(guid); + + // Assert + serviceId.Value.Should().Be(guid); + } + + [Fact] + public void Constructor_WithEmptyGuid_ShouldThrowArgumentException() + { + // Arrange + var emptyGuid = Guid.Empty; + + // Act & Assert + var act = () => new ServiceId(emptyGuid); + act.Should().Throw() + .WithMessage("ServiceId cannot be empty*"); + } + + [Fact] + public void Equals_WithSameValue_ShouldReturnTrue() + { + // Arrange + var guid = Guid.NewGuid(); + var serviceId1 = new ServiceId(guid); + var serviceId2 = new ServiceId(guid); + + // Act & Assert + serviceId1.Should().Be(serviceId2); + serviceId1.GetHashCode().Should().Be(serviceId2.GetHashCode()); + } + + [Fact] + public void Equals_WithDifferentValues_ShouldReturnFalse() + { + // Arrange + var serviceId1 = new ServiceId(Guid.NewGuid()); + var serviceId2 = new ServiceId(Guid.NewGuid()); + + // Act & Assert + serviceId1.Should().NotBe(serviceId2); + } + + [Fact] + public void ToString_ShouldReturnGuidString() + { + // Arrange + var guid = Guid.NewGuid(); + var serviceId = new ServiceId(guid); + + // Act + var result = serviceId.ToString(); + + // Assert + result.Should().Be(guid.ToString()); + } + + [Fact] + public void ValueObject_Equality_ShouldWorkCorrectly() + { + // Arrange + var guid = Guid.NewGuid(); + var serviceId1 = new ServiceId(guid); + var serviceId2 = new ServiceId(guid); + var serviceId3 = new ServiceId(Guid.NewGuid()); + + // Act & Assert + (serviceId1 == serviceId2).Should().BeTrue(); + (serviceId1 != serviceId3).Should().BeTrue(); + serviceId1.Equals(serviceId2).Should().BeTrue(); + serviceId1.Equals(serviceId3).Should().BeFalse(); + serviceId1.Equals(null).Should().BeFalse(); + } +} diff --git a/src/Shared/Contracts/Modules/ServiceCatalogs/DTOs/ServiceCatalogsModuleDtos.cs b/src/Shared/Contracts/Modules/ServiceCatalogs/DTOs/ServiceCatalogsModuleDtos.cs new file mode 100644 index 000000000..9c0c50693 --- /dev/null +++ b/src/Shared/Contracts/Modules/ServiceCatalogs/DTOs/ServiceCatalogsModuleDtos.cs @@ -0,0 +1,43 @@ +namespace MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs.DTOs; + +/// +/// DTO for service category information exposed to other modules. +/// +public sealed record ModuleServiceCategoryDto( + Guid Id, + string Name, + string? Description, + bool IsActive, + int DisplayOrder +); + +/// +/// DTO for service information exposed to other modules. +/// +public sealed record ModuleServiceDto( + Guid Id, + Guid CategoryId, + string CategoryName, + string Name, + string? Description, + bool IsActive +); + +/// +/// Simplified service DTO for list operations. +/// +public sealed record ModuleServiceListDto( + Guid Id, + Guid CategoryId, + string Name, + bool IsActive +); + +/// +/// Result of service validation operation. +/// +public sealed record ModuleServiceValidationResultDto( + bool AllValid, + Guid[] InvalidServiceIds, + Guid[] InactiveServiceIds +); diff --git a/src/Shared/Contracts/Modules/ServiceCatalogs/IServiceCatalogsModuleApi.cs b/src/Shared/Contracts/Modules/ServiceCatalogs/IServiceCatalogsModuleApi.cs new file mode 100644 index 000000000..4e28b53a9 --- /dev/null +++ b/src/Shared/Contracts/Modules/ServiceCatalogs/IServiceCatalogsModuleApi.cs @@ -0,0 +1,70 @@ +using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs.DTOs; +using MeAjudaAi.Shared.Functional; + +namespace MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs; + +/// +/// Public API contract for the ServiceCatalogs module. +/// Provides access to service categories and services catalog for other modules. +/// +public interface IServiceCatalogsModuleApi : IModuleApi +{ + // ============ Service Categories ============ + + /// + /// Retrieves a service category by ID. + /// + Task> GetServiceCategoryByIdAsync( + Guid categoryId, + CancellationToken cancellationToken = default); + + /// + /// Retrieves all service categories. + /// + /// If true, returns only active categories + /// + Task>> GetAllServiceCategoriesAsync( + bool activeOnly = true, + CancellationToken cancellationToken = default); + + // ============ Services ============ + + /// + /// Retrieves a service by ID. + /// + Task> GetServiceByIdAsync( + Guid serviceId, + CancellationToken cancellationToken = default); + + /// + /// Retrieves all services. + /// + /// If true, returns only active services + /// + Task>> GetAllServicesAsync( + bool activeOnly = true, + CancellationToken cancellationToken = default); + + /// + /// Retrieves all services in a specific category. + /// + Task>> GetServicesByCategoryAsync( + Guid categoryId, + bool activeOnly = true, + CancellationToken cancellationToken = default); + + /// + /// Checks if a service exists and is active. + /// + Task> IsServiceActiveAsync( + Guid serviceId, + CancellationToken cancellationToken = default); + + /// + /// Validates if all provided service IDs exist and are active. + /// + /// Result containing validation outcome and list of invalid service IDs + Task> ValidateServicesAsync( + IReadOnlyCollection serviceIds, + CancellationToken cancellationToken = default); +} diff --git a/tests/MeAjudaAi.Architecture.Tests/ModuleApiArchitectureTests.cs b/tests/MeAjudaAi.Architecture.Tests/ModuleApiArchitectureTests.cs index ff612f97a..d0f6ba631 100644 --- a/tests/MeAjudaAi.Architecture.Tests/ModuleApiArchitectureTests.cs +++ b/tests/MeAjudaAi.Architecture.Tests/ModuleApiArchitectureTests.cs @@ -1,6 +1,6 @@ using System.Reflection; using MeAjudaAi.Shared.Contracts.Modules; -using MeAjudaAi.Shared.Contracts.Modules.Catalogs; +using MeAjudaAi.Shared.Contracts.Modules.ServiceCatalogs; using MeAjudaAi.Shared.Contracts.Modules.Location; using MeAjudaAi.Shared.Contracts.Modules.Providers; using MeAjudaAi.Shared.Contracts.Modules.Users; @@ -288,10 +288,10 @@ public void ILocationModuleApi_ShouldHaveAllEssentialMethods() } [Fact] - public void ICatalogsModuleApi_ShouldHaveAllEssentialMethods() + public void IServiceCatalogsModuleApi_ShouldHaveAllEssentialMethods() { // Arrange - var type = typeof(ICatalogsModuleApi); + var type = typeof(IServiceCatalogsModuleApi); // Act var methods = type.GetMethods() diff --git a/tests/MeAjudaAi.E2E.Tests/Integration/CatalogsModuleIntegrationTests.cs b/tests/MeAjudaAi.E2E.Tests/Integration/ServiceCatalogsModuleIntegrationTests.cs similarity index 96% rename from tests/MeAjudaAi.E2E.Tests/Integration/CatalogsModuleIntegrationTests.cs rename to tests/MeAjudaAi.E2E.Tests/Integration/ServiceCatalogsModuleIntegrationTests.cs index ab8b90a86..6fb51892b 100644 --- a/tests/MeAjudaAi.E2E.Tests/Integration/CatalogsModuleIntegrationTests.cs +++ b/tests/MeAjudaAi.E2E.Tests/Integration/ServiceCatalogsModuleIntegrationTests.cs @@ -1,15 +1,14 @@ using System.Net; using System.Text.Json; using MeAjudaAi.E2E.Tests.Base; -using MeAjudaAi.Modules.Catalogs.Application.DTOs; namespace MeAjudaAi.E2E.Tests.Integration; /// -/// Testes de integração entre o módulo Catalogs e outros módulos +/// Testes de integração entre o módulo ServiceCatalogs e outros módulos /// Demonstra como o módulo de catálogos pode ser consumido por outros módulos /// -public class CatalogsModuleIntegrationTests : TestContainerTestBase +public class ServiceCatalogsModuleIntegrationTests : TestContainerTestBase { [Fact] public async Task ServicesModule_Can_Validate_Services_From_Catalogs() @@ -234,6 +233,10 @@ private async Task CreateServiceAsync(Guid categoryId, string name, #region DTOs + // NOTE: Local DTOs are intentionally defined here instead of importing from Application.DTOs + // to ensure E2E tests validate the actual API contract independently from internal DTOs. + // This prevents breaking changes in internal DTOs from being masked in integration tests. + private record ServiceCategoryDto( Guid Id, string Name, diff --git a/tests/MeAjudaAi.E2E.Tests/MeAjudaAi.E2E.Tests.csproj b/tests/MeAjudaAi.E2E.Tests/MeAjudaAi.E2E.Tests.csproj index 23940085d..266796c3c 100644 --- a/tests/MeAjudaAi.E2E.Tests/MeAjudaAi.E2E.Tests.csproj +++ b/tests/MeAjudaAi.E2E.Tests/MeAjudaAi.E2E.Tests.csproj @@ -39,6 +39,9 @@ + + + diff --git a/tests/MeAjudaAi.E2E.Tests/Modules/Catalogs/CatalogsEndToEndTests.cs b/tests/MeAjudaAi.E2E.Tests/Modules/ServiceCatalogs/ServiceCatalogsEndToEndTests.cs similarity index 92% rename from tests/MeAjudaAi.E2E.Tests/Modules/Catalogs/CatalogsEndToEndTests.cs rename to tests/MeAjudaAi.E2E.Tests/Modules/ServiceCatalogs/ServiceCatalogsEndToEndTests.cs index 1dcec868d..92f959204 100644 --- a/tests/MeAjudaAi.E2E.Tests/Modules/Catalogs/CatalogsEndToEndTests.cs +++ b/tests/MeAjudaAi.E2E.Tests/Modules/ServiceCatalogs/ServiceCatalogsEndToEndTests.cs @@ -1,17 +1,17 @@ using System.Text.Json; using MeAjudaAi.E2E.Tests.Base; -using MeAjudaAi.Modules.Catalogs.Application.DTOs; -using MeAjudaAi.Modules.Catalogs.Domain.Entities; -using MeAjudaAi.Modules.Catalogs.Domain.ValueObjects; -using MeAjudaAi.Modules.Catalogs.Infrastructure.Persistence; +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities; +using MeAjudaAi.Modules.ServiceCatalogs.Domain.ValueObjects; +using MeAjudaAi.Modules.ServiceCatalogs.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; -namespace MeAjudaAi.E2E.Tests.Modules.Catalogs; +namespace MeAjudaAi.E2E.Tests.Modules.ServiceCatalogs; /// -/// Testes E2E para o módulo de Catálogos usando TestContainers +/// Testes E2E para o módulo ServiceCatalogs usando TestContainers /// -public class CatalogsEndToEndTests : TestContainerTestBase +public class ServiceCatalogsEndToEndTests : TestContainerTestBase { [Fact] public async Task CreateServiceCategory_Should_Return_Success() @@ -244,7 +244,7 @@ public async Task Database_Should_Persist_ServiceCategories_Correctly() // Act - Create category directly in database await WithServiceScopeAsync(async services => { - var context = services.GetRequiredService(); + var context = services.GetRequiredService(); var category = ServiceCategory.Create(name, description, 1); categoryId = category.Id; @@ -256,7 +256,7 @@ await WithServiceScopeAsync(async services => // Assert - Verify category was persisted by ID for determinism await WithServiceScopeAsync(async services => { - var context = services.GetRequiredService(); + var context = services.GetRequiredService(); var foundCategory = await context.ServiceCategories .FirstOrDefaultAsync(c => c.Id == categoryId); @@ -278,7 +278,7 @@ public async Task Database_Should_Persist_Services_With_Category_Relationship() // Act - Create category and service await WithServiceScopeAsync(async services => { - var context = services.GetRequiredService(); + var context = services.GetRequiredService(); category = ServiceCategory.Create(Faker.Commerce.Department(), Faker.Lorem.Sentence(), 1); context.ServiceCategories.Add(category); @@ -293,7 +293,7 @@ await WithServiceScopeAsync(async services => // Assert - Verify service and category relationship by ID for determinism await WithServiceScopeAsync(async services => { - var context = services.GetRequiredService(); + var context = services.GetRequiredService(); var foundService = await context.Services .FirstOrDefaultAsync(s => s.Id == serviceId); @@ -312,7 +312,7 @@ private async Task CreateTestServiceCategoryAsync() await WithServiceScopeAsync(async services => { - var context = services.GetRequiredService(); + var context = services.GetRequiredService(); category = ServiceCategory.Create( Faker.Commerce.Department(), @@ -331,7 +331,7 @@ private async Task CreateTestServiceCategoriesAsync(int count) { await WithServiceScopeAsync(async services => { - var context = services.GetRequiredService(); + var context = services.GetRequiredService(); for (int i = 0; i < count; i++) { @@ -354,7 +354,7 @@ private async Task CreateTestServiceAsync(Guid categoryId) await WithServiceScopeAsync(async services => { - var context = services.GetRequiredService(); + var context = services.GetRequiredService(); service = Service.Create( new ServiceCategoryId(categoryId), @@ -374,7 +374,7 @@ private async Task CreateTestServicesAsync(Guid categoryId, int count) { await WithServiceScopeAsync(async services => { - var context = services.GetRequiredService(); + var context = services.GetRequiredService(); for (int i = 0; i < count; i++) { diff --git a/tests/MeAjudaAi.Integration.Tests/Base/ApiTestBase.cs b/tests/MeAjudaAi.Integration.Tests/Base/ApiTestBase.cs index 073f3b5fe..0053f29e0 100644 --- a/tests/MeAjudaAi.Integration.Tests/Base/ApiTestBase.cs +++ b/tests/MeAjudaAi.Integration.Tests/Base/ApiTestBase.cs @@ -1,6 +1,6 @@ using System.Text.Json; using MeAjudaAi.Integration.Tests.Infrastructure; -using MeAjudaAi.Modules.Catalogs.Infrastructure.Persistence; +using MeAjudaAi.Modules.ServiceCatalogs.Infrastructure.Persistence; using MeAjudaAi.Modules.Documents.Infrastructure.Persistence; using MeAjudaAi.Modules.Documents.Tests; using MeAjudaAi.Modules.Providers.Infrastructure.Persistence; @@ -51,7 +51,7 @@ public async ValueTask InitializeAsync() RemoveDbContextRegistrations(services); RemoveDbContextRegistrations(services); RemoveDbContextRegistrations(services); - RemoveDbContextRegistrations(services); + RemoveDbContextRegistrations(services); // Adiciona contextos de banco de dados para testes services.AddDbContext(options => @@ -90,12 +90,12 @@ public async ValueTask InitializeAsync() warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)); }); - services.AddDbContext(options => + services.AddDbContext(options => { options.UseNpgsql(_databaseFixture.ConnectionString, npgsqlOptions => { - npgsqlOptions.MigrationsAssembly("MeAjudaAi.Modules.Catalogs.Infrastructure"); - npgsqlOptions.MigrationsHistoryTable("__EFMigrationsHistory", "catalogs"); + npgsqlOptions.MigrationsAssembly("MeAjudaAi.Modules.ServiceCatalogs.Infrastructure"); + npgsqlOptions.MigrationsHistoryTable("__EFMigrationsHistory", "service_catalogs"); }); options.UseSnakeCaseNamingConvention(); options.EnableSensitiveDataLogging(); @@ -140,7 +140,7 @@ public async ValueTask InitializeAsync() var usersContext = scope.ServiceProvider.GetRequiredService(); var providersContext = scope.ServiceProvider.GetRequiredService(); var documentsContext = scope.ServiceProvider.GetRequiredService(); - var catalogsContext = scope.ServiceProvider.GetRequiredService(); + var catalogsContext = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetService>(); // Aplica migrações exatamente como nos testes E2E @@ -151,7 +151,7 @@ private static async Task ApplyMigrationsAsync( UsersDbContext usersContext, ProvidersDbContext providersContext, DocumentsDbContext documentsContext, - CatalogsDbContext catalogsContext, + ServiceCatalogsDbContext catalogsContext, ILogger? logger) { // Garante estado limpo do banco de dados (como nos testes E2E) @@ -170,13 +170,13 @@ private static async Task ApplyMigrationsAsync( await ApplyMigrationForContextAsync(usersContext, "Users", logger, "UsersDbContext primeiro (cria database e schema users)"); await ApplyMigrationForContextAsync(providersContext, "Providers", logger, "ProvidersDbContext (banco já existe, só precisa do schema providers)"); await ApplyMigrationForContextAsync(documentsContext, "Documents", logger, "DocumentsDbContext (banco já existe, só precisa do schema documents)"); - await ApplyMigrationForContextAsync(catalogsContext, "Catalogs", logger, "CatalogsDbContext (banco já existe, só precisa do schema catalogs)"); + await ApplyMigrationForContextAsync(catalogsContext, "ServiceCatalogs", logger, "ServiceCatalogsDbContext (banco já existe, só precisa do schema service_catalogs)"); // Verifica se as tabelas existem await VerifyContextAsync(usersContext, "Users", () => usersContext.Users.CountAsync(), logger); await VerifyContextAsync(providersContext, "Providers", () => providersContext.Providers.CountAsync(), logger); await VerifyContextAsync(documentsContext, "Documents", () => documentsContext.Documents.CountAsync(), logger); - await VerifyContextAsync(catalogsContext, "Catalogs", () => catalogsContext.ServiceCategories.CountAsync(), logger); + await VerifyContextAsync(catalogsContext, "ServiceCatalogs", () => catalogsContext.ServiceCategories.CountAsync(), logger); } public async ValueTask DisposeAsync() diff --git a/tests/MeAjudaAi.Integration.Tests/MeAjudaAi.Integration.Tests.csproj b/tests/MeAjudaAi.Integration.Tests/MeAjudaAi.Integration.Tests.csproj index 76185cb37..fe48008af 100644 --- a/tests/MeAjudaAi.Integration.Tests/MeAjudaAi.Integration.Tests.csproj +++ b/tests/MeAjudaAi.Integration.Tests/MeAjudaAi.Integration.Tests.csproj @@ -65,6 +65,10 @@ + + + + diff --git a/tests/MeAjudaAi.Integration.Tests/Modules/Catalogs/CatalogsApiTests.cs b/tests/MeAjudaAi.Integration.Tests/Modules/ServiceCatalogs/ServiceCatalogsApiTests.cs similarity index 98% rename from tests/MeAjudaAi.Integration.Tests/Modules/Catalogs/CatalogsApiTests.cs rename to tests/MeAjudaAi.Integration.Tests/Modules/ServiceCatalogs/ServiceCatalogsApiTests.cs index 1461b398d..69f4c2b31 100644 --- a/tests/MeAjudaAi.Integration.Tests/Modules/Catalogs/CatalogsApiTests.cs +++ b/tests/MeAjudaAi.Integration.Tests/Modules/ServiceCatalogs/ServiceCatalogsApiTests.cs @@ -4,13 +4,13 @@ using FluentAssertions; using MeAjudaAi.Integration.Tests.Base; -namespace MeAjudaAi.Integration.Tests.Modules.Catalogs; +namespace MeAjudaAi.Integration.Tests.Modules.ServiceCatalogs; /// -/// Testes de integração para a API do módulo Catalogs. +/// Testes de integração para a API do módulo ServiceCatalogs. /// Valida endpoints, autenticação, autorização e respostas da API. /// -public class CatalogsApiTests : ApiTestBase +public class ServiceCatalogsApiTests : ApiTestBase { [Fact] public async Task ServiceCategoriesEndpoint_ShouldBeAccessible() diff --git a/tests/MeAjudaAi.Integration.Tests/Modules/Catalogs/CatalogsDbContextTests.cs b/tests/MeAjudaAi.Integration.Tests/Modules/ServiceCatalogs/ServiceCatalogsDbContextTests.cs similarity index 80% rename from tests/MeAjudaAi.Integration.Tests/Modules/Catalogs/CatalogsDbContextTests.cs rename to tests/MeAjudaAi.Integration.Tests/Modules/ServiceCatalogs/ServiceCatalogsDbContextTests.cs index 2cc545ef4..20ea87bbb 100644 --- a/tests/MeAjudaAi.Integration.Tests/Modules/Catalogs/CatalogsDbContextTests.cs +++ b/tests/MeAjudaAi.Integration.Tests/Modules/ServiceCatalogs/ServiceCatalogsDbContextTests.cs @@ -1,25 +1,25 @@ using FluentAssertions; using MeAjudaAi.Integration.Tests.Base; -using MeAjudaAi.Modules.Catalogs.Infrastructure.Persistence; +using MeAjudaAi.Modules.ServiceCatalogs.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; -namespace MeAjudaAi.Integration.Tests.Modules.Catalogs; +namespace MeAjudaAi.Integration.Tests.Modules.ServiceCatalogs; /// /// Testes de integração para o DbContext do módulo Catalogs. /// Valida configurações do EF Core, relacionamentos e constraints. /// -public class CatalogsDbContextTests : ApiTestBase +public class ServiceCatalogsDbContextTests : ApiTestBase { [Fact] - public void CatalogsDbContext_ShouldBeRegistered() + public void ServiceCatalogsDbContext_ShouldBeRegistered() { // Arrange & Act using var scope = Services.CreateScope(); - var dbContext = scope.ServiceProvider.GetService(); + var dbContext = scope.ServiceProvider.GetService(); // Assert - dbContext.Should().NotBeNull("CatalogsDbContext should be registered in DI"); + dbContext.Should().NotBeNull("ServiceCatalogsDbContext should be registered in DI"); } [Fact] @@ -27,7 +27,7 @@ public async Task ServiceCategories_Table_ShouldExist() { // Arrange using var scope = Services.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); + var dbContext = scope.ServiceProvider.GetRequiredService(); // Act var canConnect = await dbContext.Database.CanConnectAsync(); @@ -45,7 +45,7 @@ public async Task Services_Table_ShouldExist() { // Arrange using var scope = Services.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); + var dbContext = scope.ServiceProvider.GetRequiredService(); // Act var canConnect = await dbContext.Database.CanConnectAsync(); @@ -63,10 +63,10 @@ public void Services_ShouldHaveForeignKeyToServiceCategories() { // Arrange using var scope = Services.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); + var dbContext = scope.ServiceProvider.GetRequiredService(); // Act - var serviceEntity = dbContext.Model.FindEntityType(typeof(MeAjudaAi.Modules.Catalogs.Domain.Entities.Service)); + var serviceEntity = dbContext.Model.FindEntityType(typeof(MeAjudaAi.Modules.ServiceCatalogs.Domain.Entities.Service)); var foreignKeys = serviceEntity?.GetForeignKeys(); // Assert @@ -79,13 +79,13 @@ public void CatalogsSchema_ShouldExist() { // Arrange using var scope = Services.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); + var dbContext = scope.ServiceProvider.GetRequiredService(); // Act var defaultSchema = dbContext.Model.GetDefaultSchema(); // Assert - defaultSchema.Should().Be("catalogs", "Catalogs schema should exist in database"); + defaultSchema.Should().Be("service_catalogs", "ServiceCatalogs schema should exist in database"); } [Fact] @@ -93,7 +93,7 @@ public async Task Database_ShouldAllowBasicOperations() { // Arrange using var scope = Services.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); + var dbContext = scope.ServiceProvider.GetRequiredService(); // Act & Assert - Should be able to execute queries var canConnect = await dbContext.Database.CanConnectAsync(); diff --git a/tests/MeAjudaAi.Integration.Tests/Modules/Catalogs/CatalogsDependencyInjectionTest.cs b/tests/MeAjudaAi.Integration.Tests/Modules/ServiceCatalogs/ServiceCatalogsDependencyInjectionTest.cs similarity index 80% rename from tests/MeAjudaAi.Integration.Tests/Modules/Catalogs/CatalogsDependencyInjectionTest.cs rename to tests/MeAjudaAi.Integration.Tests/Modules/ServiceCatalogs/ServiceCatalogsDependencyInjectionTest.cs index 9f0c20695..ebded76f0 100644 --- a/tests/MeAjudaAi.Integration.Tests/Modules/Catalogs/CatalogsDependencyInjectionTest.cs +++ b/tests/MeAjudaAi.Integration.Tests/Modules/ServiceCatalogs/ServiceCatalogsDependencyInjectionTest.cs @@ -1,20 +1,20 @@ using FluentAssertions; using MeAjudaAi.Integration.Tests.Base; -using MeAjudaAi.Modules.Catalogs.Application.Commands.Service; -using MeAjudaAi.Modules.Catalogs.Application.Commands.ServiceCategory; -using MeAjudaAi.Modules.Catalogs.Application.DTOs; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.Service; +using MeAjudaAi.Modules.ServiceCatalogs.Application.Commands.ServiceCategory; +using MeAjudaAi.Modules.ServiceCatalogs.Application.DTOs; using MeAjudaAi.Shared.Commands; using MeAjudaAi.Shared.Functional; using Microsoft.Extensions.DependencyInjection; -namespace MeAjudaAi.Integration.Tests.Modules.Catalogs; +namespace MeAjudaAi.Integration.Tests.Modules.ServiceCatalogs; /// -/// 🧪 TESTE DIAGNÓSTICO PARA CATALOGS MODULE DEPENDENCY INJECTION +/// 🧪 TESTE DIAGNÓSTICO PARA SERVICE CATALOGS MODULE DEPENDENCY INJECTION /// -/// Verifica se todos os command handlers do módulo Catalogs estão registrados +/// Verifica se todos os command handlers do módulo ServiceCatalogs estão registrados /// -public class CatalogsDependencyInjectionTest(ITestOutputHelper testOutput) : ApiTestBase +public class ServiceCatalogsDependencyInjectionTest(ITestOutputHelper testOutput) : ApiTestBase { [Fact] public void Should_Have_CommandDispatcher_Registered() @@ -44,7 +44,7 @@ public void Should_Have_CreateServiceCategoryCommandHandler_Registered() public void Should_Have_ServiceCategoryRepository_Registered() { // Arrange & Act - var repository = Services.GetService(); + var repository = Services.GetService(); // Assert testOutput.WriteLine($"IServiceCategoryRepository registration: {repository != null}"); @@ -53,20 +53,20 @@ public void Should_Have_ServiceCategoryRepository_Registered() } [Fact] - public void Should_Have_CatalogsDbContext_Registered() + public void Should_Have_ServiceCatalogsDbContext_Registered() { // Arrange & Act - var dbContext = Services.GetService(); + var dbContext = Services.GetService(); // Assert - testOutput.WriteLine($"CatalogsDbContext registration: {dbContext != null}"); - dbContext.Should().NotBeNull("CatalogsDbContext should be registered"); + testOutput.WriteLine($"ServiceCatalogsDbContext registration: {dbContext != null}"); + dbContext.Should().NotBeNull("ServiceCatalogsDbContext should be registered"); } [Fact] public void Should_List_All_Registered_CommandHandlers() { - // Arrange - Scan Catalogs assembly for command handler types + // Arrange - Scan ServiceCatalogs assembly for command handler types var catalogsAssembly = typeof(CreateServiceCategoryCommand).Assembly; var commandHandlerType = typeof(ICommandHandler<,>); @@ -78,10 +78,10 @@ public void Should_List_All_Registered_CommandHandlers() i.GetGenericTypeDefinition() == commandHandlerType)) .ToList(); - testOutput.WriteLine($"Found {handlerTypes.Count} command handler types in Catalogs assembly:"); + testOutput.WriteLine($"Found {handlerTypes.Count} command handler types in ServiceCatalogs assembly:"); // Assert - Verify each handler can be resolved from DI - handlerTypes.Should().NotBeEmpty("Catalogs assembly should contain command handlers"); + handlerTypes.Should().NotBeEmpty("ServiceCatalogs assembly should contain command handlers"); foreach (var handlerType in handlerTypes) { diff --git a/tests/MeAjudaAi.Integration.Tests/Modules/Catalogs/CatalogsIntegrationTests.cs b/tests/MeAjudaAi.Integration.Tests/Modules/ServiceCatalogs/ServiceCatalogsIntegrationTests.cs similarity index 98% rename from tests/MeAjudaAi.Integration.Tests/Modules/Catalogs/CatalogsIntegrationTests.cs rename to tests/MeAjudaAi.Integration.Tests/Modules/ServiceCatalogs/ServiceCatalogsIntegrationTests.cs index a77d6f5c3..2fb896c9a 100644 --- a/tests/MeAjudaAi.Integration.Tests/Modules/Catalogs/CatalogsIntegrationTests.cs +++ b/tests/MeAjudaAi.Integration.Tests/Modules/ServiceCatalogs/ServiceCatalogsIntegrationTests.cs @@ -4,13 +4,13 @@ using FluentAssertions; using MeAjudaAi.Integration.Tests.Base; -namespace MeAjudaAi.Integration.Tests.Modules.Catalogs; +namespace MeAjudaAi.Integration.Tests.Modules.ServiceCatalogs; /// -/// Testes de integração completos para o módulo Catalogs. +/// Testes de integração completos para o módulo ServiceCatalogs. /// Valida fluxos end-to-end de criação, atualização, consulta e remoção. /// -public class CatalogsIntegrationTests(ITestOutputHelper testOutput) : ApiTestBase +public class ServiceCatalogsIntegrationTests(ITestOutputHelper testOutput) : ApiTestBase { [Fact] public async Task CreateServiceCategory_WithValidData_ShouldReturnCreated() diff --git a/tests/MeAjudaAi.Integration.Tests/Modules/Catalogs/CatalogsResponseDebugTest.cs b/tests/MeAjudaAi.Integration.Tests/Modules/ServiceCatalogs/ServiceCatalogsResponseDebugTest.cs similarity index 93% rename from tests/MeAjudaAi.Integration.Tests/Modules/Catalogs/CatalogsResponseDebugTest.cs rename to tests/MeAjudaAi.Integration.Tests/Modules/ServiceCatalogs/ServiceCatalogsResponseDebugTest.cs index 1d437d02b..9f28c24cc 100644 --- a/tests/MeAjudaAi.Integration.Tests/Modules/Catalogs/CatalogsResponseDebugTest.cs +++ b/tests/MeAjudaAi.Integration.Tests/Modules/ServiceCatalogs/ServiceCatalogsResponseDebugTest.cs @@ -5,9 +5,9 @@ using MeAjudaAi.Integration.Tests.Base; using MeAjudaAi.Shared.Serialization; -namespace MeAjudaAi.Integration.Tests.Modules.Catalogs; +namespace MeAjudaAi.Integration.Tests.Modules.ServiceCatalogs; -public class CatalogsResponseDebugTest(ITestOutputHelper testOutput) : ApiTestBase +public class ServiceCatalogsResponseDebugTest(ITestOutputHelper testOutput) : ApiTestBase { [Fact(Skip = "Diagnostic test - enable only when debugging response format issues")] [Trait("Category", "Debug")]