diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fe1152b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 89bfd86..a23dc47 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,5 @@ name: CI Pipeline - Contacts Manager API -# Executa o pipeline em cada push ou pull request para a branch main on: push: branches: @@ -23,10 +22,10 @@ jobs: dotnet-version: '8.0.x' - name: Restore dependencies - run: dotnet restore + run: dotnet restore ./Contact37.sln - name: Build solution - run: dotnet build --configuration Release --no-restore + run: dotnet build ./Contact37.sln --configuration Release --no-restore - name: Run tests - run: dotnet test --configuration Release --no-build --verbosity normal + run: dotnet test ./Contact37.sln --configuration Release --no-build --verbosity normal diff --git a/Contact37.sln b/Contact37.sln index 8fece8c..95a5608 100644 --- a/Contact37.sln +++ b/Contact37.sln @@ -23,7 +23,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Contacts37.API", "Contacts3 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Contacts37.Domain.Tests", "Contacts37.Domain.Tests\Contacts37.Domain.Tests.csproj", "{2B82BA02-9D3F-45E6-88DC-8201CA210974}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Contacts37.Application.Tests", "Contacts37.Application.Tests\Contacts37.Application.Tests.csproj", "{67F02028-C088-41AC-BA06-8D95DB8F28FB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Contacts37.Application.Tests", "Contacts37.Application.Tests\Contacts37.Application.Tests.csproj", "{67F02028-C088-41AC-BA06-8D95DB8F28FB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Contacts37.Application.IntegrationTests", "Contacts37.Application.IntegrationTests\Contacts37.Application.IntegrationTests.csproj", "{334BA499-25A0-4C6B-B317-8592A2E4AD8F}" +EndProject +Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{2B09ABA7-B0CC-46E9-8133-945757E3EA12}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -55,6 +59,14 @@ Global {67F02028-C088-41AC-BA06-8D95DB8F28FB}.Debug|Any CPU.Build.0 = Debug|Any CPU {67F02028-C088-41AC-BA06-8D95DB8F28FB}.Release|Any CPU.ActiveCfg = Release|Any CPU {67F02028-C088-41AC-BA06-8D95DB8F28FB}.Release|Any CPU.Build.0 = Release|Any CPU + {334BA499-25A0-4C6B-B317-8592A2E4AD8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {334BA499-25A0-4C6B-B317-8592A2E4AD8F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {334BA499-25A0-4C6B-B317-8592A2E4AD8F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {334BA499-25A0-4C6B-B317-8592A2E4AD8F}.Release|Any CPU.Build.0 = Release|Any CPU + {2B09ABA7-B0CC-46E9-8133-945757E3EA12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B09ABA7-B0CC-46E9-8133-945757E3EA12}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B09ABA7-B0CC-46E9-8133-945757E3EA12}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B09ABA7-B0CC-46E9-8133-945757E3EA12}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -69,6 +81,7 @@ Global {F738FCAC-569A-4C0A-9E81-1B2F6901E8D3} = {52A30A57-415B-4A7C-80FA-F407E0105DAE} {2B82BA02-9D3F-45E6-88DC-8201CA210974} = {9797DF73-8B56-4F82-84A0-3F8FD0AC3ECF} {67F02028-C088-41AC-BA06-8D95DB8F28FB} = {9797DF73-8B56-4F82-84A0-3F8FD0AC3ECF} + {334BA499-25A0-4C6B-B317-8592A2E4AD8F} = {9797DF73-8B56-4F82-84A0-3F8FD0AC3ECF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {684A1232-DFE2-45B7-84F7-85F6C270F627} diff --git a/Contacts37.API/Contacts37.API.csproj b/Contacts37.API/Contacts37.API.csproj index 19acfef..6f3137d 100644 --- a/Contacts37.API/Contacts37.API.csproj +++ b/Contacts37.API/Contacts37.API.csproj @@ -1,10 +1,13 @@ - + net8.0 enable enable false + 1f76d21a-9e54-47e7-8c24-2a213f107bb0 + Linux + ..\docker-compose.dcproj @@ -14,7 +17,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Contacts37.API/Dockerfile b/Contacts37.API/Dockerfile new file mode 100644 index 0000000..6c43928 --- /dev/null +++ b/Contacts37.API/Dockerfile @@ -0,0 +1,26 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER app +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["Contacts37.API/Contacts37.API.csproj", "Contacts37.API/"] +COPY ["Contact37.Persistence/Contacts37.Persistence.csproj", "Contact37.Persistence/"] +COPY ["Contacts37.Application/Contacts37.Application.csproj", "Contacts37.Application/"] +COPY ["Contacts37.Domain/Contacts37.Domain.csproj", "Contacts37.Domain/"] +RUN dotnet restore "./Contacts37.API/./Contacts37.API.csproj" +COPY . . +WORKDIR "/src/Contacts37.API" +RUN dotnet build "./Contacts37.API.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./Contacts37.API.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Contacts37.API.dll"] \ No newline at end of file diff --git a/Contacts37.API/Extensions/MigrationExtensions.cs b/Contacts37.API/Extensions/MigrationExtensions.cs new file mode 100644 index 0000000..88caf9d --- /dev/null +++ b/Contacts37.API/Extensions/MigrationExtensions.cs @@ -0,0 +1,17 @@ +using Contacts37.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace Contacts37.API.Extensions +{ + public static class MigrationExtensions + { + public static void ApplyMigrations(this IApplicationBuilder app) + { + using IServiceScope scope = app.ApplicationServices.CreateScope(); + + var dbContext = scope.ServiceProvider.GetRequiredService(); + + dbContext.Database.Migrate(); + } + } +} diff --git a/Contacts37.API/Program.cs b/Contacts37.API/Program.cs index 677a5de..dadde03 100644 --- a/Contacts37.API/Program.cs +++ b/Contacts37.API/Program.cs @@ -1,6 +1,9 @@ +using Contacts37.API.Extensions; using Contacts37.API.Middlewares; using Contacts37.Application.DependencyInjection; using Contacts37.Persistence.DependencyInjection; +using Prometheus; +using Serilog; var builder = WebApplication.CreateBuilder(args); @@ -8,6 +11,10 @@ .ConfigurePersistenceServices(builder.Configuration) .ConfigureApplicationServices(); +builder.Host.UseSerilog((context, config) => + config.ReadFrom.Configuration(context.Configuration) +); + builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -19,6 +26,7 @@ { app.UseSwagger(); app.UseSwaggerUI(); + app.ApplyMigrations(); } app.UseMiddleware(); @@ -27,6 +35,12 @@ app.UseAuthorization(); +app.UseMetricServer(); + +app.UseHttpMetrics(); + app.MapControllers(); app.Run(); + +public partial class Program { } diff --git a/Contacts37.API/Properties/launchSettings.json b/Contacts37.API/Properties/launchSettings.json index 35e3a69..52f7316 100644 --- a/Contacts37.API/Properties/launchSettings.json +++ b/Contacts37.API/Properties/launchSettings.json @@ -1,33 +1,24 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:38899", - "sslPort": 44371 - } - }, +{ "profiles": { "http": { "commandName": "Project", - "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", - "applicationUrl": "http://localhost:5070", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5070" }, "https": { "commandName": "Project", - "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", - "applicationUrl": "https://localhost:7202;http://localhost:5070", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7202;http://localhost:5070" }, "IIS Express": { "commandName": "IISExpress", @@ -36,6 +27,26 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:38899", + "sslPort": 44371 } } -} +} \ No newline at end of file diff --git a/Contacts37.API/appsettings.json b/Contacts37.API/appsettings.json index 8d358b9..f596e4a 100644 --- a/Contacts37.API/appsettings.json +++ b/Contacts37.API/appsettings.json @@ -1,12 +1,23 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, "AllowedHosts": "*", "ConnectionStrings": { - "Contact37ConnectionString": "Server=(localdb)\\MSSQLLocalDB;Database=ContactManager37Db;Trusted_Connection=True;MultipleActiveResultSets=true" + "Contact37ConnectionString": "Server=mssql;Database=ContactManager37Db;User Id=sa;Password=P@ssw0rd1;TrustServerCertificate=yes" + }, + "Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ], + "MinimumLevel": "Debug", + "WriteTo": [ + { + "Name": "Console" + }, + { + "Name": "Seq", + "Args": { "serverUrl": "http://seq-logs" } + } + ], + "Enrich": [ "FromLogContext", "WithMachineName" ], + "Properties": { + "ApplicationName": "Contacts Manager API" + } } } diff --git a/Contacts37.Application.IntegrationTests/Contacts/ContactsTests.cs b/Contacts37.Application.IntegrationTests/Contacts/ContactsTests.cs new file mode 100644 index 0000000..8c847e0 --- /dev/null +++ b/Contacts37.Application.IntegrationTests/Contacts/ContactsTests.cs @@ -0,0 +1,139 @@ +using Contacts37.Application.Common.Exceptions; +using Contacts37.Application.IntegrationTests.Fixtures; +using Contacts37.Application.IntegrationTests.Infrastructure; +using Contacts37.Application.Usecases.Contacts.Commands.Update; +using Contacts37.Application.Usecases.Contacts.Queries.GetAll; +using Contacts37.Application.Usecases.Contacts.Queries.GetByDdd; +using Contacts37.Domain.Entities; +using FluentAssertions; + +namespace Contacts37.Application.IntegrationTests.Contacts +{ + [Collection(nameof(IntegrationTestCollection))] + public class ContactsTests : BaseIntegrationTest + { + private readonly ContactFixture _fixture; + public ContactsTests(IntegrationTestWebAppFactory factory, ContactFixture fixture) : base(factory) + { + _fixture = fixture; + } + + + [Fact(DisplayName = "Should create new contact with valid values")] + [Trait("Category", "Integration")] + [Trait("Component", "Database")] + public async Task CreateContactCommand_ShouldAddNewContact_WhenValid() + { + //Arrange + var command = _fixture.CreateValidContactCommand(); + + //Act + var result = await Sender.Send(command); + + //Assert + var contact = DbContext.Contacts.FirstOrDefault(c => c.Id == result.Id); + + contact.Should().NotBeNull() + .And.BeOfType(); + contact!.Name.Should().NotBeNullOrWhiteSpace() + .And.Be(command.Name); + contact.Region.DddCode.Should().BeGreaterThan(0) + .And.Be(command.DDDCode); + contact.Phone.Should().HaveLength(9) + .And.Be(command.Phone); + contact.Email.Should().NotBeNullOrWhiteSpace() + .And.Be(command.Email); + } + + [Fact(DisplayName = "Should throw exception when creating new contact with invalid values")] + [Trait("Category", "Integration")] + [Trait("Component", "Database")] + public async Task CreateContactCommand_ShouldThrowException_WhenPhoneIsInvalid() + { + //Arrange + var command = _fixture.CreateInvalidContactCommand(); + + //Act + Func act = async () => await Sender.Send(command); + + //Assert + await act.Should().ThrowAsync(); + } + + [Fact(DisplayName = "Should update existing contact with valid values")] + [Trait("Category", "Integration")] + [Trait("Component", "Database")] + public async Task UpdateContactCommand_ShouldUpdateContact_WhenIsValid() + { + //Arrange + var existingContact = DataSeeder.GetTestContact(); + var command = new UpdateContactCommand(existingContact.Id, "Ayrton", 11, "987654321", "ayrton.senna@example.com"); + + //Act + await Sender.Send(command); + + //Assert + var contact = DbContext.Contacts.FirstOrDefault(c => c.Id == existingContact.Id); + + contact.Should().NotBeNull() + .And.BeOfType(); + contact!.Name.Should().NotBeNullOrWhiteSpace() + .And.Be("Ayrton"); + contact.Region.DddCode.Should().BeGreaterThan(0) + .And.Be(11); + contact.Phone.Should().HaveLength(9) + .And.Be("987654321"); + contact.Email.Should().NotBeNullOrWhiteSpace() + .And.Be("ayrton.senna@example.com"); + } + + [Fact(DisplayName = "Should return existing contacts from database")] + [Trait("Category", "Integration")] + [Trait("Component", "Database")] + public async Task GetAllContactsCommand_ShouldReturnContactsList_FromDatabase() + { + //Act + var result = await Sender.Send(new GetAllContactsRequest()); + + //Assert + result.Should().NotBeNull(); + result.Should().HaveCount(5) + .And.AllBeOfType(); + } + + [Fact(DisplayName = "Should return one contact that exists on database")] + [Trait("Category", "Integration")] + [Trait("Component", "Database")] + public async Task GetContactsByDdd_ShouldReturnContacts_WhenContactsExists() + { + //Arrange + var existingContact = DataSeeder.GetTestContact(); + var query = new GetContactsByDddRequest(existingContact.Region.DddCode); + + //Act + var result = await Sender.Send(query); + + //Assert + result.Should().NotBeNull() + .And.AllBeOfType() + .And.ContainSingle(contact => contact.Name == existingContact.Name + && contact.DDDCode == existingContact.Region.DddCode); + } + + [Fact(DisplayName = "Should return empty list of contact from the database")] + [Trait("Category", "Integration")] + [Trait("Component", "Database")] + public async Task GetByDdd_ShouldReturnEmpty_WhenNoContactsForDddCode() + { + // Arrange + var request = new GetContactsByDddRequest(99); + + // Act + var result = await Sender.Send(request); + + // Assert + result.Should().NotBeNull() + .And.BeEmpty(); + } + } +} diff --git a/Contacts37.Application.IntegrationTests/Contacts37.Application.IntegrationTests.csproj b/Contacts37.Application.IntegrationTests/Contacts37.Application.IntegrationTests.csproj new file mode 100644 index 0000000..b9d78be --- /dev/null +++ b/Contacts37.Application.IntegrationTests/Contacts37.Application.IntegrationTests.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Contacts37.Application.IntegrationTests/Fixtures/ContactFixture.cs b/Contacts37.Application.IntegrationTests/Fixtures/ContactFixture.cs new file mode 100644 index 0000000..b5e8ee0 --- /dev/null +++ b/Contacts37.Application.IntegrationTests/Fixtures/ContactFixture.cs @@ -0,0 +1,33 @@ +using Bogus; +using Contacts37.Application.Usecases.Contacts.Commands.Create; + +namespace Contacts37.Application.IntegrationTests.Fixtures +{ + public class ContactFixture + { + private readonly Faker _faker; + + public ContactFixture() + { + _faker = new Faker(); + } + + public CreateContactCommand CreateValidContactCommand() + { + return new CreateContactCommand( + _faker.Person.FirstName, + _faker.PickRandom(new[] { 11, 21, 31, 41 }), + _faker.Phone.PhoneNumber("#########"), + _faker.Person.Email); + } + + public CreateContactCommand CreateInvalidContactCommand() + { + return new CreateContactCommand( + _faker.Person.FirstName, + _faker.PickRandom(new[] { 11, 21, 31, 41 }), + _faker.Phone.PhoneNumber("###"), + _faker.Person.Email); + } + } +} diff --git a/Contacts37.Application.IntegrationTests/GlobalUsings.cs b/Contacts37.Application.IntegrationTests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/Contacts37.Application.IntegrationTests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/Contacts37.Application.IntegrationTests/Infrastructure/BaseIntegrationTest.cs b/Contacts37.Application.IntegrationTests/Infrastructure/BaseIntegrationTest.cs new file mode 100644 index 0000000..efb7873 --- /dev/null +++ b/Contacts37.Application.IntegrationTests/Infrastructure/BaseIntegrationTest.cs @@ -0,0 +1,40 @@ +using Contacts37.Application.IntegrationTests.TestData; +using Contacts37.Persistence; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Contacts37.Application.IntegrationTests.Infrastructure +{ + public abstract class BaseIntegrationTest : IClassFixture, IAsyncLifetime + { + private readonly IServiceScope _scope; + + protected BaseIntegrationTest(IntegrationTestWebAppFactory factory) + { + _scope = factory.Services.CreateScope(); + + Sender = _scope.ServiceProvider.GetRequiredService(); + + DbContext = _scope.ServiceProvider.GetRequiredService(); + + DataSeeder = new TestDataSeeder(DbContext); + + } + + protected readonly ISender Sender; + protected readonly ApplicationDbContext DbContext; + protected readonly TestDataSeeder DataSeeder; + + + public Task InitializeAsync() + { + return DataSeeder.SeedAsync(); + } + + public Task DisposeAsync() + { + return DbContext.Database.ExecuteSqlRawAsync("DELETE FROM Contacts"); + } + } +} diff --git a/Contacts37.Application.IntegrationTests/Infrastructure/IntegrationTestCollection.cs b/Contacts37.Application.IntegrationTests/Infrastructure/IntegrationTestCollection.cs new file mode 100644 index 0000000..cea82cd --- /dev/null +++ b/Contacts37.Application.IntegrationTests/Infrastructure/IntegrationTestCollection.cs @@ -0,0 +1,6 @@ +using Contacts37.Application.IntegrationTests.Fixtures; + +namespace Contacts37.Application.IntegrationTests.Infrastructure; + +[CollectionDefinition(nameof(IntegrationTestCollection))] +public class IntegrationTestCollection : ICollectionFixture, ICollectionFixture; diff --git a/Contacts37.Application.IntegrationTests/Infrastructure/IntegrationTestWebAppFactory.cs b/Contacts37.Application.IntegrationTests/Infrastructure/IntegrationTestWebAppFactory.cs new file mode 100644 index 0000000..bac45be --- /dev/null +++ b/Contacts37.Application.IntegrationTests/Infrastructure/IntegrationTestWebAppFactory.cs @@ -0,0 +1,49 @@ +using Contacts37.Persistence; +using DotNet.Testcontainers.Builders; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Testcontainers.MsSql; + +namespace Contacts37.Application.IntegrationTests.Infrastructure +{ + public class IntegrationTestWebAppFactory : WebApplicationFactory, IAsyncLifetime + { + private readonly MsSqlContainer _dbContainer; + + public IntegrationTestWebAppFactory() + { + _dbContainer = new MsSqlBuilder() + .WithImage("mcr.microsoft.com/mssql/server:2022-latest") + .WithPassword("StrongP@ssword123!") + .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(1433)) + .Build(); + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureTestServices(services => + { + // Remove o DbContext configurado pela aplicação + var descriptor = services + .SingleOrDefault(s => s.ServiceType == typeof(DbContextOptions)); + + if (descriptor is not null) + { + services.Remove(descriptor); + } + + // Registra o DbContext usando a connection string do Testcontainers + services.AddDbContext(options => + { + options.UseSqlServer(_dbContainer.GetConnectionString()); + }); + }); + } + + public Task InitializeAsync() => _dbContainer.StartAsync(); + public new Task DisposeAsync() => _dbContainer.StopAsync(); + } +} diff --git a/Contacts37.Application.IntegrationTests/TestData/TestDataSeeder.cs b/Contacts37.Application.IntegrationTests/TestData/TestDataSeeder.cs new file mode 100644 index 0000000..2445eb9 --- /dev/null +++ b/Contacts37.Application.IntegrationTests/TestData/TestDataSeeder.cs @@ -0,0 +1,44 @@ +using Bogus; +using Contacts37.Domain.Entities; +using Contacts37.Persistence; + +namespace Contacts37.Application.IntegrationTests.TestData; + +public class TestDataSeeder +{ + private readonly ApplicationDbContext _context; + private readonly Faker _faker; + + // Keep track of seeded entity for test reference + private readonly List _contacts = new(); + + public TestDataSeeder(ApplicationDbContext context) + { + _context = context; + _faker = new Faker(); + } + + public async Task SeedAsync() + { + await SeedContacts(); + await _context.SaveChangesAsync(); + } + + private async Task SeedContacts() + { + var contactList = Enumerable.Range(1, 5) + .Select(_ => Contact.Create( + _faker.Name.FirstName(), + _faker.PickRandom(new[] { 11, 21, 31, 41 }), + _faker.Phone.PhoneNumber("#########"), + _faker.Internet.Email() + )) + .ToList(); + + _contacts.AddRange(contactList); + await _context.Contacts.AddRangeAsync(contactList); + } + + // Helper method to get test entity + public Contact GetTestContact(int index = 0) => _contacts[index]; +} diff --git a/Contacts37.Application.Tests/Fixtures/ContactFixture.cs b/Contacts37.Application.Tests/Fixtures/ContactFixture.cs new file mode 100644 index 0000000..d6a64a4 --- /dev/null +++ b/Contacts37.Application.Tests/Fixtures/ContactFixture.cs @@ -0,0 +1,129 @@ +using AutoMapper; +using Bogus; +using Contacts37.Application.Usecases.Contacts.Commands.Create; +using Contacts37.Application.Usecases.Contacts.Commands.Update; +using Contacts37.Application.Usecases.Contacts.Queries.GetAll; +using Contacts37.Application.Usecases.Contacts.Queries.GetByDdd; +using Contacts37.Domain.Entities; + +namespace Contacts37.Application.Tests.Fixtures +{ + public class ContactFixture + { + public IMapper Mapper { get; } + private readonly Faker _faker; + private readonly IEnumerable ValidDddCodes; + + public ContactFixture() + { + Mapper = ConfigureMapper(); + _faker = new Faker(); + + ValidDddCodes = new[] { 11, 21, 31, 41 }; + } + + private static IMapper ConfigureMapper() + { + var config = new MapperConfiguration(cfg => + { + cfg.AddProfile(); + cfg.AddProfile(); + cfg.AddProfile(); + }); + return new Mapper(config); + } + + public Contact CreateValidContact() + { + return Contact.Create( + _faker.Person.FirstName, + _faker.PickRandom(ValidDddCodes), + _faker.Phone.PhoneNumber("#########"), + _faker.Person.Email); + } + + public IEnumerable CreateValidContactList(int count = 5) + { + return Enumerable.Range(1, count) + .Select(_ => CreateValidContact()) + .ToList(); + } + + public CreateContactCommand CreateContactCommandFromEntity(Contact contact) + { + return new CreateContactCommand( + contact.Name, + contact.Region.DddCode, + contact.Phone, + contact.Email); + } + + public CreateContactCommand CreateValidContactCommand() + { + return new CreateContactCommand( + _faker.Person.FirstName, + _faker.PickRandom(ValidDddCodes), + _faker.Phone.PhoneNumber("#########"), + _faker.Person.Email); + } + + public CreateContactCommand CreateContactCommandWithInvalidData(string? name = null, int? dddCode = null, string? phone = null, string? email = null) + { + return new CreateContactCommand( + name ?? " ", + dddCode ?? _faker.PickRandom(ValidDddCodes), + phone ?? _faker.Phone.PhoneNumber("###"), + email); + } + + public CreateContactCommand CreateValidContactCommandWithEmailNull() + { + return new CreateContactCommand( + _faker.Person.FirstName, + _faker.PickRandom(ValidDddCodes), + _faker.Phone.PhoneNumber("#########")); + } + + public UpdateContactCommand CreateValidUpdateContactCommand(Contact contact) + { + return new UpdateContactCommand( + contact.Id, + contact.Name, + contact.Region.DddCode, + contact.Phone, + contact.Email); + } + + public UpdateContactCommand CreateValidUpdateContactCommandWithNewPhone(Contact contact) + { + return new UpdateContactCommand( + contact.Id, + contact.Name, + _faker.PickRandom(ValidDddCodes), + _faker.Phone.PhoneNumber("###"), + contact.Email); + } + + public UpdateContactCommand CreateValidUpdateContactCommandForNonExistentContact() + { + var contact = CreateValidContact(); + + return new UpdateContactCommand( + contact.Id, + contact.Name, + contact.Region.DddCode, + contact.Phone, + contact.Email); + } + + public UpdateContactCommand CreateInvalidUpdateContactCommandWithInvalidEmail(Contact contact) + { + return new UpdateContactCommand( + contact.Id, + contact.Name, + contact.Region.DddCode, + contact.Phone, + "invalid_email"); + } + } +} diff --git a/Contacts37.Application.Tests/Fixtures/ContactFixtureCollection.cs b/Contacts37.Application.Tests/Fixtures/ContactFixtureCollection.cs new file mode 100644 index 0000000..e19593c --- /dev/null +++ b/Contacts37.Application.Tests/Fixtures/ContactFixtureCollection.cs @@ -0,0 +1,4 @@ +namespace Contacts37.Application.Tests.Fixtures; + +[CollectionDefinition(nameof(ContactFixtureCollection))] +public class ContactFixtureCollection : ICollectionFixture; diff --git a/Contacts37.Application.Tests/Usecases/Contacts/Commands/Create/CreateContactCommandHandlerTests.cs b/Contacts37.Application.Tests/Usecases/Contacts/Commands/Create/CreateContactCommandHandlerTests.cs index f413563..cc8ce99 100644 --- a/Contacts37.Application.Tests/Usecases/Contacts/Commands/Create/CreateContactCommandHandlerTests.cs +++ b/Contacts37.Application.Tests/Usecases/Contacts/Commands/Create/CreateContactCommandHandlerTests.cs @@ -1,7 +1,6 @@ -using AutoMapper; -using Bogus; -using Contacts37.Application.Common.Exceptions; +using Contacts37.Application.Common.Exceptions; using Contacts37.Application.Contracts.Persistence; +using Contacts37.Application.Tests.Fixtures; using Contacts37.Application.Usecases.Contacts.Commands.Create; using Contacts37.Domain.Entities; using Contacts37.Domain.Exceptions; @@ -10,174 +9,183 @@ namespace Contacts37.Application.Tests.Usecases.Contacts.Commands.Create { + [Collection(nameof(ContactFixtureCollection))] public class CreateContactCommandHandlerTests { - private readonly IMapper _mapper; private readonly Mock _contactRepositoryMock; - private readonly Faker _faker; - public CreateContactCommandHandlerTests() + private readonly CreateContactCommandHandler _handler; + private readonly ContactFixture _fixture; + + public CreateContactCommandHandlerTests(ContactFixture fixture) { _contactRepositoryMock = new Mock(); - _faker = new Faker(); - - var config = new MapperConfiguration(cfg => cfg.AddProfile()); - _mapper = new Mapper(config); - + _handler = new CreateContactCommandHandler(_contactRepositoryMock.Object, fixture.Mapper); + _fixture = fixture; } - [Fact(DisplayName = "Validate create contact")] - [Trait("Category", "Create Contact - Sucess")] - public async void CreateContact_ShouldSucess_WhenDataIsValidAndUnique() + [Fact(DisplayName = "Should create a contact successfully when data is valid and unique")] + [Trait("Category", "Create Contact - Success")] + public async Task CreateContact_ShouldSucess_WhenDataIsValidAndUnique() { - // Arrange - string name = _faker.Person.FirstName; - int dddCode = _faker.PickRandom(new[] { 11, 21, 31, 41 }); - string phone = _faker.Phone.PhoneNumber("#########"); - string email = _faker.Internet.Email(); + // Arrange + var contact = _fixture.CreateValidContact(); + var command = _fixture.CreateContactCommandFromEntity(contact); - var command = new CreateContactCommand(name, dddCode, phone, email); - - _contactRepositoryMock.Setup(repo => repo.IsEmailUniqueAsync(command.Email!)) + _contactRepositoryMock.Setup(repo => repo.IsEmailUniqueAsync(contact.Email!)) .ReturnsAsync(true); - _contactRepositoryMock.Setup(repo => repo.IsDddAndPhoneUniqueAsync(command.DDDCode , command.Phone)) + _contactRepositoryMock.Setup(repo => repo.IsDddAndPhoneUniqueAsync(contact.Region.DddCode, contact.Phone)) .ReturnsAsync(true); - //_contactRepositoryMock.Setup(repo => repo.AddAsync(contact)) - // .ReturnsAsync(true); - - var handler = new CreateContactCommandHandler(_contactRepositoryMock.Object, _mapper); + _contactRepositoryMock.Setup(repo => repo.AddAsync(It.IsAny())) + .ReturnsAsync(1); // Act - var result = await handler.Handle(command, CancellationToken.None); + var result = await _handler.Handle(command, CancellationToken.None); // Assert result.Should().NotBeNull(); result.Should().BeOfType(); + result.Id.Should().NotBeEmpty(); + + _contactRepositoryMock.Verify(repo => repo.IsEmailUniqueAsync(command.Email!), Times.Once); + _contactRepositoryMock.Verify(repo => repo.IsDddAndPhoneUniqueAsync(command.DDDCode, command.Phone), Times.Once); _contactRepositoryMock.Verify(repo => repo.AddAsync(It.IsAny()), Times.Once); } - //[Fact(DisplayName = "Validate contact when invalid name")] - //[Trait("Category", "Create Contact - Failure - Invalid Name")] - //public async void CreateContact_ShouldThrowException_WhenContactInvalidName() - //{ - // // Arrange - // string name = " "; - // int dddCode = _faker.PickRandom(new[] { 11, 21, 31, 41 }); - // string phone = _faker.Phone.PhoneNumber("#########"); - // string email = _faker.Internet.Email(); - // var contact = Contact.Create(name, dddCode, phone, email); + [Fact(DisplayName = "Should create a contact when email is null")] + [Trait("Category", "Create Contact - Success")] + public async void CreateContact_ShouldSucess_WhenEmailIsNull() + { + // Arrange + var command = _fixture.CreateValidContactCommandWithEmailNull(); - // var handler = new CreateContactCommandHandler(_contactRepositoryMock.Object, _mapper); + _contactRepositoryMock.Setup(repo => repo.IsDddAndPhoneUniqueAsync(command.DDDCode, command.Phone)).ReturnsAsync(true); + _contactRepositoryMock.Setup(repo => repo.AddAsync(It.IsAny())).ReturnsAsync(1); - // var command = new CreateContactCommand(name, dddCode, phone, email); + // Act + var result = await _handler.Handle(command, CancellationToken.None); - // // Act - // Func act = async () => await handler.Handle(command, CancellationToken.None); + // Assert + result.Should().NotBeNull(); + result.Should().BeOfType(); + _contactRepositoryMock.Verify(repo => repo.AddAsync(It.IsAny()), Times.Once); + } - // // Assert - // await act.Should().ThrowAsync() - // .WithMessage($"Name is required."); - // _contactRepositoryMock.Verify(repo => repo.GetAsync(contact.Id), Times.Never); - // _contactRepositoryMock.Verify(repo => repo.UpdateAsync(contact), Times.Never); - //} + [Fact(DisplayName = "Should fail to create contact when Phone is not unique")] + [Trait("Category", "Create Contact - Failure - Phone already exists")] + public async void CreateContact_ShouldThrowException_WhenPhoneIsNotUnique() + { + // Arrange + var command = _fixture.CreateValidContactCommand(); - //[Fact(DisplayName = "Validate contact when invalid fone")] - //[Trait("Category", "Create Contact - Failure - Invalid Fone")] - //public async void CreateContact_ShouldThrowException_WhenContactInvalidFone() - //{ - // // Arrange - // string name = _faker.Person.FirstName; - // int dddCode = _faker.PickRandom(new[] { 11, 21, 31, 41 }); - // string phone = _faker.Phone.PhoneNumber("###"); - // string email = _faker.Internet.Email(); + _contactRepositoryMock.Setup(repo => repo.IsEmailUniqueAsync(command.Email!)) + .ReturnsAsync(true); - // var contact = Contact.Create(name, dddCode, phone, email); + _contactRepositoryMock.Setup(repo => repo.IsDddAndPhoneUniqueAsync(command.DDDCode, command.Phone)) + .ReturnsAsync(false); - // var handler = new CreateContactCommandHandler(_contactRepositoryMock.Object, _mapper); + // Act + Func act = async () => await _handler.Handle(command, CancellationToken.None); - // var command = new CreateContactCommand(name, dddCode, phone, email); + // Assert + await act.Should().ThrowAsync() + .WithMessage($"A contact with the same DDD '{command.DDDCode}' and phone '{command.Phone}' already exists."); - // // Act - // Func act = async () => await handler.Handle(command, CancellationToken.None); + _contactRepositoryMock.Verify(repo => repo.IsDddAndPhoneUniqueAsync(command.DDDCode, command.Phone), Times.Once); + _contactRepositoryMock.Verify(repo => repo.AddAsync(It.IsAny()), Times.Never); + } - // // Assert - // await act.Should().ThrowAsync() - // .WithMessage($"Phone '{phone}' must be a 9-digit number."); - // _contactRepositoryMock.Verify(repo => repo.GetAsync(contact.Id), Times.Once); - // _contactRepositoryMock.Verify(repo => repo.UpdateAsync(contact), Times.Never); - //} + [Fact(DisplayName = "Should fail to create contact when email is not unique")] + [Trait("Category", "Create Contact - Failure - Email already exists")] + public async Task CreateContact_ShouldThrowException_WhenEmailIsNotUnique() + { + // Arrange + var command = _fixture.CreateValidContactCommand(); - //[Fact(DisplayName = "Validate contact when email exist")] - //[Trait("Category", "Create Contact - Failure - Email Exist")] - //public async void CreateContact_ShouldThrowException_WhenContactExistEmail() - //{ - // // Arrange - // string name = _faker.Person.FirstName; - // int dddCode = _faker.PickRandom(new[] { 11, 21, 31, 41 }); - // string phone = _faker.Phone.PhoneNumber("#########"); - // string email = _faker.Internet.Email(); + _contactRepositoryMock.Setup(repo => repo.IsEmailUniqueAsync(command.Email!)) + .ReturnsAsync(false); - // var contact = Contact.Create(name, dddCode, phone, email); + // Act + Func act = async () => await _handler.Handle(command, CancellationToken.None); - // int dddCode2 = _faker.PickRandom(new[] { 11, 21, 31, 41 }); - // string phone2 = _faker.Phone.PhoneNumber("#########"); + // Assert + await act.Should().ThrowAsync() + .WithMessage($"A contact with the same Email '{command.Email}' already exists."); - // var handler = new CreateContactCommandHandler(_contactRepositoryMock.Object, _mapper); + _contactRepositoryMock.Verify(repo => repo.IsEmailUniqueAsync(command.Email!), Times.Once); + _contactRepositoryMock.Verify(repo => repo.AddAsync(It.IsAny()), Times.Never); + } - // var command = new CreateContactCommand(name, dddCode2, phone2, email); + [Fact(DisplayName = "Should fail to create contact when name is invalid")] + [Trait("Category", "Create Contact - Failure - Invalid Name")] + public async Task CreateContact_ShouldThrowException_WhenNameIsInvalid() + { + // Arrange + var command = _fixture.CreateContactCommandWithInvalidData(); - // _contactRepositoryMock.Setup(repo => repo.GetAsync(contact.Id)) - // .ReturnsAsync(contact); + _contactRepositoryMock.Setup(repo => repo.IsDddAndPhoneUniqueAsync(command.DDDCode, command.Phone)) + .ReturnsAsync(true); - // _contactRepositoryMock.Setup(repo => repo.DeleteAsync(contact)) - // .Returns(Task.CompletedTask); + // Act + Func act = async () => await _handler.Handle(command, CancellationToken.None); - // // Act - // Func act = async () => await handler.Handle(command, CancellationToken.None); + // Assert + await act.Should().ThrowAsync() + .WithMessage("Name is required."); - // // Assert - // await act.Should().ThrowAsync() - // .WithMessage($"A contact with the same Email '{email}' already exists."); - // _contactRepositoryMock.Verify(repo => repo.GetAsync(contact.Id), Times.Once); - // _contactRepositoryMock.Verify(repo => repo.UpdateAsync(contact), Times.Never); - //} + _contactRepositoryMock.Verify(repo => repo.IsDddAndPhoneUniqueAsync(command.DDDCode, command.Phone), Times.Once); + _contactRepositoryMock.Verify(repo => repo.AddAsync(It.IsAny()), Times.Never); + } - //[Fact(DisplayName = "Validate contact when invalid email")] - //[Trait("Category", "Create Contact - Failure - Invalid Email")] - //public async void CreateContact_ShouldThrowException_WhenContactInvalidEmail() - //{ - // // Arrange - // string name = _faker.Person.FirstName; - // int dddCode = _faker.PickRandom(new[] { 11, 21, 31, 41 }); - // string phone = _faker.Phone.PhoneNumber("#########"); - // string email = _faker.Internet.Email(); + [Fact(DisplayName = "Should fail to create contact when email is invalid")] + [Trait("Category", "Create Contact - Failure - Invalid Email")] + public async Task CreateContact_ShouldThrowException_WhenEmailIsInvalid() + { + // Arrange + var invalidEmail = "invalid_email"; + var contact = _fixture.CreateValidContact(); + var command = _fixture.CreateContactCommandWithInvalidData(contact.Name, contact.Region.DddCode, contact.Phone, invalidEmail); - // var contact = Contact.Create(name, dddCode, phone, email); + _contactRepositoryMock.Setup(repo => repo.IsEmailUniqueAsync(command.Email!)) + .ReturnsAsync(true); + _contactRepositoryMock.Setup(repo => repo.IsDddAndPhoneUniqueAsync(command.DDDCode, command.Phone)) + .ReturnsAsync(true); - // var handler = new CreateContactCommandHandler(_contactRepositoryMock.Object, _mapper); + // Act + Func act = async () => await _handler.Handle(command, CancellationToken.None); - // var command = new CreateContactCommand(name, dddCode, phone, "email123"); + // Assert + await act.Should().ThrowAsync() + .WithMessage($"Email '{command.Email}' must be a valid format."); + + _contactRepositoryMock.Verify(repo => repo.IsEmailUniqueAsync(command.Email!), Times.Once); + _contactRepositoryMock.Verify(repo => repo.IsDddAndPhoneUniqueAsync(command.DDDCode, command.Phone), Times.Once); + _contactRepositoryMock.Verify(repo => repo.AddAsync(It.IsAny()), Times.Never); + } - // _contactRepositoryMock.Setup(repo => repo.GetAsync(contact.Id)) - // .ReturnsAsync(contact); + [Fact(DisplayName = "Should fail to create contact when phone is invalid")] + [Trait("Category", "Create Contact - Failure - Invalid Phone")] + public async Task CreateContact_ShouldThrowException_WhenPhoneIsInvalid() + { + // Arrange + var contact = _fixture.CreateValidContact(); + var command = _fixture.CreateContactCommandWithInvalidData(contact.Name); - // _contactRepositoryMock.Setup(repo => repo.IsEmailUniqueAsync(command.Email!)) - // .ReturnsAsync(true); + _contactRepositoryMock.Setup(repo => repo.IsDddAndPhoneUniqueAsync(command.DDDCode, command.Phone)) + .ReturnsAsync(true); - // _contactRepositoryMock.Setup(repo => repo.DeleteAsync(contact)) - // .Returns(Task.CompletedTask); + // Act + Func act = async () => await _handler.Handle(command, CancellationToken.None); - // // Act - // Func act = async () => await handler.Handle(command, CancellationToken.None); + // Assert + await act.Should().ThrowAsync() + .WithMessage($"Phone '{command.Phone}' must be a 9-digit number."); - // // Assert - // await act.Should().ThrowAsync() - // .WithMessage($"Email '{command.Email!}' must be a valid format."); - // _contactRepositoryMock.Verify(repo => repo.GetAsync(contact.Id), Times.Once); - // _contactRepositoryMock.Verify(repo => repo.UpdateAsync(contact), Times.Never); - //} + _contactRepositoryMock.Verify(repo => repo.IsDddAndPhoneUniqueAsync(command.DDDCode, command.Phone), Times.Once); + _contactRepositoryMock.Verify(repo => repo.AddAsync(It.IsAny()), Times.Never); + } } } diff --git a/Contacts37.Application.Tests/Usecases/Contacts/Commands/Delete/DeleteContactCommandHandlerTests.cs b/Contacts37.Application.Tests/Usecases/Contacts/Commands/Delete/DeleteContactCommandHandlerTests.cs index cee2905..265cca8 100644 --- a/Contacts37.Application.Tests/Usecases/Contacts/Commands/Delete/DeleteContactCommandHandlerTests.cs +++ b/Contacts37.Application.Tests/Usecases/Contacts/Commands/Delete/DeleteContactCommandHandlerTests.cs @@ -1,6 +1,6 @@ -using Bogus; -using Contacts37.Application.Common.Exceptions; +using Contacts37.Application.Common.Exceptions; using Contacts37.Application.Contracts.Persistence; +using Contacts37.Application.Tests.Fixtures; using Contacts37.Application.Usecases.Contacts.Commands.Delete; using Contacts37.Domain.Entities; using FluentAssertions; @@ -8,30 +8,26 @@ namespace Contacts37.Application.Tests.Usecases.Contacts.Commands.Delete { + [Collection(nameof(ContactFixtureCollection))] public class DeleteContactCommandHandlerTests { private readonly Mock _contactRepositoryMock; private readonly DeleteContactCommandHandler _handler; - private readonly Faker _faker; + private readonly ContactFixture _fixture; - public DeleteContactCommandHandlerTests() + public DeleteContactCommandHandlerTests(ContactFixture fixture) { _contactRepositoryMock = new Mock(); _handler = new DeleteContactCommandHandler(_contactRepositoryMock.Object); - _faker = new Faker(); + _fixture = fixture; } [Fact(DisplayName = "Validate contact deletion when contact exists")] [Trait("Category", "Delete Contact - Success")] - public async void DeleteContact_ShouldSucceed_WhenContactExists() + public async Task DeleteContact_ShouldSucceed_WhenContactExists() { // Arrange - string name = _faker.Person.FirstName; - int dddCode = _faker.PickRandom(new[] { 11, 21, 31, 41 }); - string phone = _faker.Phone.PhoneNumber("#########"); - string email = _faker.Internet.Email(); - - var contact = Contact.Create(name, dddCode, phone, email); + var contact = _fixture.CreateValidContact(); _contactRepositoryMock.Setup(repo => repo.GetAsync(contact.Id)) .ReturnsAsync(contact); @@ -52,10 +48,10 @@ public async void DeleteContact_ShouldSucceed_WhenContactExists() [Fact(DisplayName = "Validate contact deletion when contact does not exists")] [Trait("Category", "Delete Contact - Failure")] - public async void DeleteContact_ShouldThrowException_WhenContactDoesNotExists() + public async Task DeleteContact_ShouldThrowException_WhenContactDoesNotExists() { // Arrange - var contactId = _faker.Random.Guid(); + var contactId = _fixture.CreateValidContact().Id; _contactRepositoryMock.Setup(repo => repo.GetAsync(contactId)) .ReturnsAsync((Contact)null); diff --git a/Contacts37.Application.Tests/Usecases/Contacts/Commands/Put/UpdateContactCommandHandlerTests.cs b/Contacts37.Application.Tests/Usecases/Contacts/Commands/Put/UpdateContactCommandHandlerTests.cs index 259c3e4..6a7ccb7 100644 --- a/Contacts37.Application.Tests/Usecases/Contacts/Commands/Put/UpdateContactCommandHandlerTests.cs +++ b/Contacts37.Application.Tests/Usecases/Contacts/Commands/Put/UpdateContactCommandHandlerTests.cs @@ -1,78 +1,83 @@ -using AutoMapper; -using AutoMapper.Configuration; -using Bogus; -using Bogus.DataSets; -using Contacts37.Application.Common.Exceptions; +using Contacts37.Application.Common.Exceptions; using Contacts37.Application.Contracts.Persistence; +using Contacts37.Application.Tests.Fixtures; using Contacts37.Application.Usecases.Contacts.Commands.Update; using Contacts37.Domain.Entities; using Contacts37.Domain.Exceptions; -using Contacts37.Domain.ValueObjects; using FluentAssertions; +using MediatR; using Moq; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Reflection.Metadata; -using System.Text; -using System.Threading.Tasks; + namespace Contacts37.Application.Tests.Usecases.Contacts.Commands.Put { + [Collection(nameof(ContactFixtureCollection))] public class UpdateContactCommandHandlerTests { private readonly Mock _contactRepositoryMock; private readonly UpdateContactCommandHandler _handler; - private readonly Faker _faker; - public UpdateContactCommandHandlerTests() + private readonly ContactFixture _fixture; + + public UpdateContactCommandHandlerTests(ContactFixture fixture) { _contactRepositoryMock = new Mock(); _handler = new UpdateContactCommandHandler(_contactRepositoryMock.Object); - _faker = new Faker(); - + _fixture = fixture; } [Fact(DisplayName = "Validate contact update when contact exists")] - [Trait("Category", "Update Contact - Sucess")] - public async void UpdateContact_ShouldSucess_WhenContactExists() + [Trait("Category", "Update Contact - Success")] + public async Task UpdateContact_ShouldSucess_WhenContactExists() { // Arrange - string name = _faker.Person.FirstName; - int dddCode = _faker.PickRandom(new[] { 11, 21, 31, 41 }); - string phone = _faker.Phone.PhoneNumber("#########"); - string email = _faker.Internet.Email(); + var existingContact = _fixture.CreateValidContact(); + var command = _fixture.CreateValidUpdateContactCommand(existingContact); - var contact = Contact.Create(name, dddCode, phone, email); + _contactRepositoryMock.Setup(repo => repo.GetAsync(existingContact.Id)) + .ReturnsAsync(existingContact); - _contactRepositoryMock.Setup(repo => repo.GetAsync(contact.Id)) - .ReturnsAsync(contact); + _contactRepositoryMock.Setup(repo => repo.IsDddAndPhoneUniqueAsync(existingContact.Region.DddCode, existingContact.Phone)) + .ReturnsAsync(true); + + _contactRepositoryMock.Setup(repo => repo.IsEmailUniqueAsync(existingContact.Email!)) + .ReturnsAsync(true); - _contactRepositoryMock.Setup(repo => repo.AddAsync(contact)); + _contactRepositoryMock.Setup(repo => repo.UpdateAsync(existingContact)) + .Returns(Task.CompletedTask); - var command = new UpdateContactCommand(contact.Id, name, dddCode, phone, email); // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + result.Should().Be(Unit.Value); + _contactRepositoryMock.Verify(repo => repo.UpdateAsync(It.IsAny()), Times.Once); + } + + [Fact(DisplayName = "Should throw Exception when contact is not found")] + [Trait("Category", "Update Contact - Failure")] + public async Task UpdateContact_ShouldThrowException_WhenContactDoesNotExist() + { + // Arrange + var command = _fixture.CreateValidUpdateContactCommandForNonExistentContact(); + + _contactRepositoryMock.Setup(repo => repo.GetAsync(It.IsAny())) + .ReturnsAsync((Contact)null); + Func act = async () => await _handler.Handle(command, CancellationToken.None); // Assert - await act.Should().NotThrowAsync(); - _contactRepositoryMock.Verify(repo => repo.GetAsync(contact.Id), Times.Once); - _contactRepositoryMock.Verify(repo => repo.UpdateAsync(contact), Times.Once); + await act.Should().ThrowAsync() + .WithMessage($"Contact with ID '{command.Id}' not found."); } + [Fact(DisplayName = "Validate contact update when invalid email")] [Trait("Category", "Update Contact - Failure - Invalid Email")] public async void UpdateContact_ShouldThrowException_WhenContactInvalidEmail() { // Arrange - string name = _faker.Person.FirstName; - int dddCode = _faker.PickRandom(new[] { 11, 21, 31, 41 }); - string phone = _faker.Phone.PhoneNumber("#########"); - string email = _faker.Internet.Email(); - - var contact = Contact.Create(name, dddCode, phone, email); - - var command = new UpdateContactCommand(contact.Id, name, dddCode, phone, "email123"); + var contact = _fixture.CreateValidContact(); + var command = _fixture.CreateInvalidUpdateContactCommandWithInvalidEmail(contact); _contactRepositoryMock.Setup(repo => repo.GetAsync(contact.Id)) .ReturnsAsync(contact); @@ -80,7 +85,7 @@ public async void UpdateContact_ShouldThrowException_WhenContactInvalidEmail() _contactRepositoryMock.Setup(repo => repo.IsEmailUniqueAsync(command.Email!)) .ReturnsAsync(true); - _contactRepositoryMock.Setup(repo => repo.AddAsync(contact)); + _contactRepositoryMock.Setup(repo => repo.UpdateAsync(contact)); // Act Func act = async () => await _handler.Handle(command, CancellationToken.None); @@ -92,39 +97,31 @@ await act.Should().ThrowAsync() _contactRepositoryMock.Verify(repo => repo.UpdateAsync(contact), Times.Never); } - [Fact(DisplayName = "Validate contact update when duplicated fone")] - [Trait("Category", "Update Contact - Failure - Duplicated Fone")] - public async void UpdateContact_ShouldThrowException_WhenContactDuplicatedFone() + [Fact(DisplayName = "Validate contact update when phone is not unique")] + [Trait("Category", "Update Contact - Failure - Duplicated Phone")] + public async void UpdateContact_ShouldThrowException_WhenContactDuplicatedPhone() { // Arrange - string name = _faker.Person.FirstName; - int dddCode = _faker.PickRandom(new[] { 11, 21, 31, 41 }); - string phone = _faker.Phone.PhoneNumber("#########"); - string email = _faker.Internet.Email(); - - var contact1 = Contact.Create(name, dddCode, phone, email); - _contactRepositoryMock.Setup(repo => repo.GetAsync(contact1.Id)) - .ReturnsAsync(contact1); - _contactRepositoryMock.Setup(repo => repo.AddAsync(contact1)); - - name = _faker.Person.FirstName; - email = _faker.Internet.Email(); - dddCode = _faker.PickRandom(new[] { 11, 21, 31, 41 }); - phone = _faker.Phone.PhoneNumber("#########"); - var contact2 = Contact.Create(name, dddCode, phone, email); - _contactRepositoryMock.Setup(repo => repo.GetAsync(contact2.Id)) - .ReturnsAsync(contact2); - _contactRepositoryMock.Setup(repo => repo.AddAsync(contact2)); - - var command = new UpdateContactCommand(contact2.Id, name, contact1.Region.DddCode, contact1.Phone, email); + var contact = _fixture.CreateValidContact(); + var command = _fixture.CreateValidUpdateContactCommandWithNewPhone(contact); + + _contactRepositoryMock.Setup(repo => repo.GetAsync(contact.Id)) + .ReturnsAsync(contact); + + _contactRepositoryMock.Setup(repo => repo.IsDddAndPhoneUniqueAsync(command.DDDCode, command.Phone)) + .ReturnsAsync(false); + + _contactRepositoryMock.Setup(repo => repo.UpdateAsync(contact)); + // Act Func act = async () => await _handler.Handle(command, CancellationToken.None); // Assert await act.Should().ThrowAsync() - .WithMessage($"A contact with the same DDD '{contact1.Region.DddCode}' and phone '{contact1.Phone}' already exists."); - _contactRepositoryMock.Verify(repo => repo.GetAsync(contact2.Id), Times.Once); - _contactRepositoryMock.Verify(repo => repo.UpdateAsync(contact2), Times.Never); + .WithMessage($"A contact with the same DDD '{command.DDDCode}' and phone '{command.Phone}' already exists."); + _contactRepositoryMock.Verify(repo => repo.GetAsync(contact.Id), Times.Once); + _contactRepositoryMock.Verify(repo => repo.IsDddAndPhoneUniqueAsync(command.DDDCode, command.Phone), Times.Once); + _contactRepositoryMock.Verify(repo => repo.UpdateAsync(contact), Times.Never); } } } diff --git a/Contacts37.Application.Tests/Usecases/Contacts/Queries/GetAllContactsByDddRequestHandlerTests.cs b/Contacts37.Application.Tests/Usecases/Contacts/Queries/GetAllContactsByDddRequestHandlerTests.cs new file mode 100644 index 0000000..22f9545 --- /dev/null +++ b/Contacts37.Application.Tests/Usecases/Contacts/Queries/GetAllContactsByDddRequestHandlerTests.cs @@ -0,0 +1,66 @@ +using Contacts37.Application.Contracts.Persistence; +using Contacts37.Application.Tests.Fixtures; +using Contacts37.Application.Usecases.Contacts.Queries.GetByDdd; +using Contacts37.Domain.Entities; +using FluentAssertions; +using Moq; + +namespace Contacts37.Application.Tests.Usecases.Contacts.Queries +{ + [Collection(nameof(ContactFixtureCollection))] + public class GetAllContactsByDddRequestHandlerTests + { + private readonly Mock _contactRepositoryMock; + private readonly GetContactsByDddRequestHandler _handler; + private readonly ContactFixture _fixture; + + public GetAllContactsByDddRequestHandlerTests(ContactFixture fixture) + { + _contactRepositoryMock = new Mock(); + _handler = new GetContactsByDddRequestHandler(_contactRepositoryMock.Object, fixture.Mapper); + _fixture = fixture; + } + + [Fact(DisplayName = "Should list all contacts filtered by DDD successfully when data exists")] + [Trait("Category", "Get All Contacts by DDD - Success")] + public async void GetAllContactsByDdd_ShouldSucceed_WhenContactsExist() + { + //Arrange + var selectedDdd = 11; + + var contacts = _fixture.CreateValidContactList().Select(c => + { + c.UpdateRegion(11); + return c; + }).ToList(); + + _contactRepositoryMock.Setup(repo => repo.GetContactsDddCode(selectedDdd)) + .ReturnsAsync(contacts); + + // Act + var result = await _handler.Handle(new GetContactsByDddRequest(selectedDdd), CancellationToken.None); + + //Assert + result.Should().BeOfType>(); + result.Should().HaveCount(contacts.Count); + } + + [Fact(DisplayName = "Should return an empty list of contacts filtered by unexisting DDD")] + [Trait("Category", "Get All Contacts by DDD - Success")] + public async void GetAllContactsByDdd_ShouldSucceed_WhenNoContactsExist() + { + //Arrange + var selectedDdd = 23; + + _contactRepositoryMock.Setup(repo => repo.GetContactsDddCode(selectedDdd)) + .ReturnsAsync(new List()); + + // Act + var result = await _handler.Handle(new GetContactsByDddRequest(selectedDdd), CancellationToken.None); + + //Assert + result.Should().BeOfType>(); + result.Should().BeEmpty(); + } + } +} diff --git a/Contacts37.Application.Tests/Usecases/Contacts/Queries/GetAllContactsRequestHandlerTests.cs b/Contacts37.Application.Tests/Usecases/Contacts/Queries/GetAllContactsRequestHandlerTests.cs index c746101..294b1c9 100644 --- a/Contacts37.Application.Tests/Usecases/Contacts/Queries/GetAllContactsRequestHandlerTests.cs +++ b/Contacts37.Application.Tests/Usecases/Contacts/Queries/GetAllContactsRequestHandlerTests.cs @@ -1,58 +1,42 @@ -using AutoMapper; -using Bogus; -using Contacts37.Application.Contracts.Persistence; +using Contacts37.Application.Contracts.Persistence; +using Contacts37.Application.Tests.Fixtures; using Contacts37.Application.Usecases.Contacts.Queries.GetAll; using Contacts37.Domain.Entities; using FluentAssertions; using Moq; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Contacts37.Application.Tests.Usecases.Contacts.Queries { + [Collection(nameof(ContactFixtureCollection))] public class GetAllContactsRequestHandlerTests { private readonly Mock _contactRepositoryMock; - private readonly Faker _faker; - private readonly IMapper _mapper; + private readonly GetAllContactsRequestHandler _handler; + private readonly ContactFixture _fixture; - public GetAllContactsRequestHandlerTests() + public GetAllContactsRequestHandlerTests(ContactFixture fixture) { _contactRepositoryMock = new Mock(); - _faker = new Faker(); - var config = new MapperConfiguration(cfg => cfg.AddProfile()); - _mapper = config.CreateMapper(); + _handler = new GetAllContactsRequestHandler(_contactRepositoryMock.Object, fixture.Mapper); + _fixture = fixture; } - [Fact(DisplayName = "Validate get all contacts")] + [Fact(DisplayName = "Should list all contacts successfully when data exists")] [Trait("Category", "Get All Contacts - Success")] public async void GetAllContacts_ShouldSucceed_WhenContactsExist() { //Arrange - string name = _faker.Person.FirstName; - int dddCode = _faker.PickRandom(new[] { 11, 21, 31, 41 }); - string phone = _faker.Phone.PhoneNumber("#########"); - string email = _faker.Internet.Email(); - - var contacts = new List - { - Contact.Create(name, dddCode, phone, email) - }; + var contacts = _fixture.CreateValidContactList(); _contactRepositoryMock.Setup(repo => repo.GetAllAsync()) .ReturnsAsync(contacts); - var handler = new GetAllContactsRequestHandler(_contactRepositoryMock.Object, _mapper); - // Act - var result = await handler.Handle(new GetAllContactsRequest(), CancellationToken.None); + var result = await _handler.Handle(new GetAllContactsRequest(), CancellationToken.None); //Assert result.Should().BeOfType>(); - result.Should().HaveCount(1); + result.Should().HaveCount(5); } [Fact(DisplayName = "Validate get all contacts with empty list")] @@ -63,10 +47,8 @@ public async void GetAllContacts_ShouldSucceed_WhenNoContactsExist() _contactRepositoryMock.Setup(repo => repo.GetAllAsync()) .ReturnsAsync(new List()); - var handler = new GetAllContactsRequestHandler(_contactRepositoryMock.Object, _mapper); - // Act - var result = await handler.Handle(new GetAllContactsRequest(), CancellationToken.None); + var result = await _handler.Handle(new GetAllContactsRequest(), CancellationToken.None); //Assert result.Should().BeOfType>(); diff --git a/Contacts37.Domain.Tests/Contacts37.Domain.Tests.csproj b/Contacts37.Domain.Tests/Contacts37.Domain.Tests.csproj index b4da0c9..53b71bd 100644 --- a/Contacts37.Domain.Tests/Contacts37.Domain.Tests.csproj +++ b/Contacts37.Domain.Tests/Contacts37.Domain.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -10,6 +10,7 @@ + diff --git a/Contacts37.Domain.Tests/Entities/ContactTests.cs b/Contacts37.Domain.Tests/Entities/ContactTests.cs index 4cfd6f2..906f7f2 100644 --- a/Contacts37.Domain.Tests/Entities/ContactTests.cs +++ b/Contacts37.Domain.Tests/Entities/ContactTests.cs @@ -1,33 +1,40 @@ using Contacts37.Domain.Entities; using Contacts37.Domain.Exceptions; +using Contacts37.Domain.Tests.Fixtures; using FluentAssertions; namespace Contacts37.Domain.Tests.Entities { + [Collection(nameof(ContactFixtureCollection))] public class ContactTests { - [Fact(DisplayName = "Validate contact creation with valid parameters")] + private readonly ContactFixture _fixture; + + public ContactTests(ContactFixture fixture) + { + _fixture = fixture; + } + + [Fact(DisplayName = "Should create new contact with valid values")] [Trait("Category", "Create Contact - Success")] public void CreateContact_ValidParameters_ShouldCreate() { // Arrange - string name = "Jony"; - int dddCode = 11; - string phone = "123456789"; - string email = "jony@example.com"; - - // Act - var contact = Contact.Create(name, dddCode, phone, email); + var contact = _fixture.CreateValidContact(); // Assert contact.Should().NotBeNull(); - contact.Name.Should().Be(name); - contact.Region.DddCode.Should().Be(dddCode); - contact.Phone.Should().Be(phone); - contact.Email.Should().Be(email); + contact.Name.Should().NotBeNullOrWhiteSpace(); + contact.Name.Should().Be(contact.Name); + contact.Region.DddCode.Should().BeGreaterThan(0); + contact.Region.DddCode.Should().Be(contact.Region.DddCode); + contact.Phone.Should().MatchRegex(@"^\d{9}$"); + contact.Phone.Should().Be(contact.Phone); + contact.Email.Should().MatchRegex(@"^[^@\s]+@[^@\s]+\.[^@\s]+$"); + contact.Email.Should().Be(contact.Email); } - [Theory(DisplayName = "Validate contact creation with invalid parameters")] + [Theory(DisplayName = "Should throw exception when creating new contact with invalid values")] [Trait("Category", "Create Contact - Failure")] [InlineData("", 11, "987654321", "jony@example.com", "Name is required.")] [InlineData(null, 11, "987654321", "jony@example.com", "Name is required.")] @@ -49,12 +56,12 @@ public void CreateContact_InvalidParameters_ShouldThrowException( .WithMessage(expectedErrorMessage); } - [Fact(DisplayName = "Validate updating name with valid name")] + [Fact(DisplayName = "Should update name when valid name is provided")] [Trait("Category", "Update Contact Name - Success")] public void UpdateContact_ValidName_ShouldUpdateName() { // Arrange - var contact = Contact.Create("Jony", 11, "123456789", "jony@example.com"); + var contact = _fixture.CreateValidContact(); string newName = "Jane"; // Act @@ -64,14 +71,14 @@ public void UpdateContact_ValidName_ShouldUpdateName() contact.Name.Should().Be(newName); } - [Theory(DisplayName = "Validate updating name with invalid names")] + [Theory(DisplayName = "Should throw exception when updating name with invalid value")] [Trait("Category", "Update Contact Name - Failure")] [InlineData(null)] [InlineData("")] public void UpdateContact_InvalidName_ShouldThrowException(string invalidName) { // Arrange - var contact = Contact.Create("Jony", 11, "123456789", "jony@example.com"); + var contact = _fixture.CreateValidContact(); // Act Action act = () => contact.UpdateName(invalidName); @@ -81,12 +88,12 @@ public void UpdateContact_InvalidName_ShouldThrowException(string invalidName) .WithMessage("Name is required."); } - [Fact(DisplayName = "Validate updating phone with valid phone")] + [Fact(DisplayName = "Should update phone when valid phone is provided")] [Trait("Category", "Update Contact Phone - Success")] public void UpdateContact_ValidPhone_ShouldUpdatePhone() { // Arrange - var contact = Contact.Create("Jony", 11, "123456789", "jony@example.com"); + var contact = _fixture.CreateValidContact(); string newPhone = "987654321"; // Act @@ -96,7 +103,7 @@ public void UpdateContact_ValidPhone_ShouldUpdatePhone() contact.Phone.Should().Be(newPhone); } - [Theory(DisplayName = "Validate updating phone with invalid phones")] + [Theory(DisplayName = "Should throw exception when updating phone with invalid value")] [Trait("Category", "Update Contact Phone - Failure")] [InlineData(null)] [InlineData("")] @@ -105,7 +112,7 @@ public void UpdateContact_ValidPhone_ShouldUpdatePhone() public void UpdateContact_InvalidPhones_ShouldThrowException(string invalidPhone) { // Arrange - var contact = Contact.Create("Jony", 11, "123456789", "jony@example.com"); + var contact = _fixture.CreateValidContact(); // Act Action act = () => contact.UpdatePhone(invalidPhone); @@ -115,12 +122,13 @@ public void UpdateContact_InvalidPhones_ShouldThrowException(string invalidPhone .WithMessage($"Phone '{invalidPhone}' must be a 9-digit number."); } - [Fact(DisplayName = "Validate updating email with valid email")] + [Fact(DisplayName = "Should update email when valid email is provided")] [Trait("Category", "Update Contact Email - Success")] public void UpdateContact_ValidEmail_ShouldUpdateEmail() { // Arrange - var contact = Contact.Create("Jony", 11, "123456789", null); + var contact = _fixture.CreateValidContact(); + string newEmail = "jony@example.com"; // Act @@ -130,5 +138,21 @@ public void UpdateContact_ValidEmail_ShouldUpdateEmail() contact.Email.Should().Be(newEmail); contact.Email.Should().NotBeNull(); } + + [Theory(DisplayName = "Should throw exception when updating email with invalid value")] + [Trait("Category", "Update Contact Email - Failure")] + [InlineData("jony@example")] + public void UpdateContact_InvalidEmails_ShouldThrowException(string invalidEmail) + { + // Arrange + var contact = Contact.Create("Jony", 11, "123456789", null); + + // Act + Action act = () => contact.UpdateEmail(invalidEmail); + + // Assert + act.Should().Throw() + .WithMessage($"Email '{invalidEmail}' must be a valid format."); + } } } diff --git a/Contacts37.Domain.Tests/Fixtures/ContactFixture.cs b/Contacts37.Domain.Tests/Fixtures/ContactFixture.cs new file mode 100644 index 0000000..468df74 --- /dev/null +++ b/Contacts37.Domain.Tests/Fixtures/ContactFixture.cs @@ -0,0 +1,24 @@ +using Bogus; +using Contacts37.Domain.Entities; + +namespace Contacts37.Domain.Tests.Fixtures +{ + public class ContactFixture + { + private readonly Faker _faker; + + public ContactFixture() + { + _faker = new Faker(); + } + + public Contact CreateValidContact() + { + return Contact.Create( + _faker.Person.FirstName, + _faker.PickRandom(new[] { 11, 21, 31, 41 }), + _faker.Phone.PhoneNumber("#########"), + _faker.Person.Email); + } + } +} diff --git a/Contacts37.Domain.Tests/Fixtures/ContactFixtureCollection.cs b/Contacts37.Domain.Tests/Fixtures/ContactFixtureCollection.cs new file mode 100644 index 0000000..cdb2c52 --- /dev/null +++ b/Contacts37.Domain.Tests/Fixtures/ContactFixtureCollection.cs @@ -0,0 +1,7 @@ +namespace Contacts37.Domain.Tests.Fixtures +{ + [CollectionDefinition(nameof(ContactFixtureCollection))] + public class ContactFixtureCollection : ICollectionFixture + { + } +} diff --git a/Contacts37.Domain.Tests/ValueObjects/RegionTests.cs b/Contacts37.Domain.Tests/ValueObjects/RegionTests.cs index b28fbc2..14d252a 100644 --- a/Contacts37.Domain.Tests/ValueObjects/RegionTests.cs +++ b/Contacts37.Domain.Tests/ValueObjects/RegionTests.cs @@ -21,7 +21,9 @@ public void CreateRegion_ValidDddCode_ShouldCreateRegion(int dddCode, string nam // Assert region.Should().NotBeNull(); region.DddCode.Should().Be(dddCode); + region.DddCode.Should().BeGreaterThan(0); region.Name.Should().Be(name); + region.Name.Should().NotBeNullOrEmpty(); } [Theory(DisplayName = "Validate region creation with invalid DDD codes")] diff --git a/docker-compose.dcproj b/docker-compose.dcproj new file mode 100644 index 0000000..62de834 --- /dev/null +++ b/docker-compose.dcproj @@ -0,0 +1,19 @@ + + + + 2.1 + Linux + False + 2b09aba7-b0cc-46e9-8133-945757e3ea12 + LaunchBrowser + {Scheme}://localhost:{ServicePort}/swagger + contacts37.api + + + + docker-compose.yml + + + + + \ No newline at end of file diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..7199f2f --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,10 @@ +version: '3.4' + +services: + contacts37.api: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://*:7202 + volumes: + - ${APPDATA}/Microsoft/UserSecrets:/home/app/.microsoft/usersecrets:ro + - ${APPDATA}/ASP.NET/Https:/home/app/.aspnet/https:ro \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a7015d9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,79 @@ +services: + contacts37.api: + image: ${DOCKER_REGISTRY-}contacts37api + container_name: contacts37-api + build: + context: . + dockerfile: Contacts37.API/Dockerfile + ports: + - "7202:7202" + depends_on: [contacts37.database, contacts37.seq_logs] + restart: unless-stopped + networks: + - contacts-network + + contacts37.database: + image: mcr.microsoft.com/mssql/server:2022-latest + container_name: contacts37-database + hostname: mssql + environment: + - ACCEPT_EULA=Y + - MSSQL_SA_PASSWORD=P@ssw0rd1 + - MSSQL_PID=Express + ports: + - "1410:1433" + volumes: + - sql-data:/var/opt/mssql + restart: unless-stopped + networks: + - contacts-network + + contacts37.seq_logs: + image: datalust/seq + container_name: contacts37-logs + hostname: seq-logs + environment: + - ACCEPT_EULA=Y + volumes: + - seq-data:/data + ports: + - "5342:80" + restart: unless-stopped + networks: + - contacts-network + + contacts37.prometheus: + image: prom/prometheus:latest + container_name: contacts37-prometheus + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus-data:/prometheus + ports: + - "9090:9090" + restart: unless-stopped + networks: + - contacts-network + + contacts37.grafana: + image: grafana/grafana:latest + container_name: contacts37-grafana + ports: + - "3000:3000" + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + volumes: + - grafana-data:/var/lib/grafana + restart: unless-stopped + networks: + - contacts-network + +networks: + contacts-network: + driver: bridge + +volumes: + sql-data: + seq-data: + prometheus-data: + grafana-data: diff --git a/launchSettings.json b/launchSettings.json new file mode 100644 index 0000000..0e26219 --- /dev/null +++ b/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "Docker Compose": { + "commandName": "DockerCompose", + "commandVersion": "1.0", + "serviceActions": { + "contacts37.api": "StartDebugging" + } + } + } +} \ No newline at end of file diff --git a/prometheus.yml b/prometheus.yml new file mode 100644 index 0000000..e382d43 --- /dev/null +++ b/prometheus.yml @@ -0,0 +1,8 @@ +global: + scrape_interval: 5s + +scrape_configs: + - job_name: "contacts37_api" + metrics_path: "/metrics" + static_configs: + - targets: ["contacts37.api:7202"]