diff --git a/IGroceryStore.sln.DotSettings.user b/IGroceryStore.sln.DotSettings.user index 88d78ab..385822b 100644 --- a/IGroceryStore.sln.DotSettings.user +++ b/IGroceryStore.sln.DotSettings.user @@ -4,11 +4,11 @@ <PhysicalFolder Path="/Users/adrianfranczak/.nuget/packages/opentelemetry.instrumentation.http/1.0.0-rc9.5" Loaded="True" /> <PhysicalFolder Path="/Users/adrianfranczak/.nuget/packages/opentelemetry.instrumentation.http/1.0.0-rc9.6" Loaded="True" /> </AssemblyExplorer> - /Users/adrianfranczak/Library/Caches/JetBrains/Rider2022.2/resharper-host/temp/Rider/vAny/CoverageData/_IGroceryStore.1213299661/Snapshot/snapshot.utdcvr - <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from &lt;tests&gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <Project Location="/Users/adrianfranczak/Repos/Private/IGroceryStore" Presentation="&lt;tests&gt;" /> + + <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from &lt;tests&gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <Project Location="\Users\adrianfranczak\Repos\Private\IGroceryStore" Presentation="&lt;tests&gt;" /> </SessionState> True diff --git a/src/API/Middlewares/ExceptionMiddleware.cs b/src/API/Middlewares/ExceptionMiddleware.cs index 036aa43..d76cd9e 100644 --- a/src/API/Middlewares/ExceptionMiddleware.cs +++ b/src/API/Middlewares/ExceptionMiddleware.cs @@ -66,4 +66,4 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next) } private static string ToUnderscoreCase(string value) => string.Concat(value.Select((x, i) => i > 0 && char.IsUpper(x) && !char.IsUpper(value[i-1]) ? $"_{x}" : x.ToString())).ToLower(); -} \ No newline at end of file +} diff --git a/src/API/Program.cs b/src/API/Program.cs index 5ab8d8f..866102a 100644 --- a/src/API/Program.cs +++ b/src/API/Program.cs @@ -25,10 +25,10 @@ } //AWS -if (!builder.Environment.IsDevelopment() && !builder.Environment.IsTestEnvironment()) -{ - builder.Configuration.AddSystemsManager("/Production/IGroceryStore", TimeSpan.FromSeconds(30)); -} +// if (!builder.Environment.IsDevelopment() && !builder.Environment.IsTestEnvironment()) +// { +// builder.Configuration.AddSystemsManager("/Production/IGroceryStore", TimeSpan.FromSeconds(30)); +// } //DateTime builder.Services.AddSingleton(); diff --git a/src/API/Properties/launchSettings.json b/src/API/Properties/launchSettings.json index fda9cd1..5edded3 100644 --- a/src/API/Properties/launchSettings.json +++ b/src/API/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Postman": { "commandName": "Project", - "launchBrowser": false, + "launchBrowser": true, "applicationUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/src/API/appsettings.json b/src/API/appsettings.json index 01c64cd..dc03290 100644 --- a/src/API/appsettings.json +++ b/src/API/appsettings.json @@ -14,7 +14,7 @@ ] }, "ElasticConfiguration": { - "Uri": "foo" + "Uri": "http://localhost:9200" }, "Postgres": { "ConnectionString": "foo", diff --git a/src/Baskets/Baskets.Core/Baskets.Core.csproj b/src/Baskets/Baskets.Core/Baskets.Core.csproj index 2f612bc..451e4e5 100644 --- a/src/Baskets/Baskets.Core/Baskets.Core.csproj +++ b/src/Baskets/Baskets.Core/Baskets.Core.csproj @@ -27,4 +27,8 @@ + + + + diff --git a/src/Products/Products.Contracts/Events/ProductUpdated.cs b/src/Products/Products.Contracts/Events/ProductUpdated.cs new file mode 100644 index 0000000..02d7767 --- /dev/null +++ b/src/Products/Products.Contracts/Events/ProductUpdated.cs @@ -0,0 +1,3 @@ +namespace IGroceryStore.Products.Contracts.Events; + +public record class ProductUpdated(ulong Id, string Name); diff --git a/src/Products/Products.Core/Features/Categories/Commands/UpdateCategory.cs b/src/Products/Products.Core/Features/Categories/Commands/UpdateCategory.cs index 438864b..bbc79ce 100644 --- a/src/Products/Products.Core/Features/Categories/Commands/UpdateCategory.cs +++ b/src/Products/Products.Core/Features/Categories/Commands/UpdateCategory.cs @@ -22,7 +22,7 @@ public class UpdateCategoryEndpoint : IEndpoint { public void RegisterEndpoint(IEndpointRouteBuilder endpoints) => endpoints.MapPut("api/categories/{id}") - .AddEndpointFilter>() + .AddEndpointFilter>() .WithTags(SwaggerTags.Products) .Produces(202) .Produces(400); @@ -55,11 +55,11 @@ await _productsDbContext.Categories } } -internal class UpdateCategoryValidator : AbstractValidator +internal class UpdateCategoryValidator : AbstractValidator { public UpdateCategoryValidator() { - RuleFor(x => x.Name) + RuleFor(x => x.Body.Name) .MinimumLength(3) .NotEmpty(); } diff --git a/src/Products/Products.Core/Features/Products/Commands/CreateProduct.cs b/src/Products/Products.Core/Features/Products/Commands/CreateProduct.cs index 88b9ff8..302fc66 100644 --- a/src/Products/Products.Core/Features/Products/Commands/CreateProduct.cs +++ b/src/Products/Products.Core/Features/Products/Commands/CreateProduct.cs @@ -11,7 +11,6 @@ using IGroceryStore.Shared.Services; using IGroceryStore.Shared.Validation; using MassTransit; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; @@ -32,9 +31,11 @@ public class CreateProductEndpoint : IEndpoint { public void RegisterEndpoint(IEndpointRouteBuilder endpoints) => endpoints.MapPost("api/products") - .RequireAuthorization() - .AddEndpointFilter>() - .WithTags(SwaggerTags.Products); + //.RequireAuthorization() + .AddEndpointFilter>() + .WithTags(SwaggerTags.Products) + .Produces(400) + .Produces(202); } internal class CreateProductHandler : ICommandHandler @@ -81,32 +82,32 @@ public async Task HandleAsync(CreateProduct command, CancellationToken } } -internal class CreateProductValidator : AbstractValidator +internal class CreateProductValidator : AbstractValidator { public CreateProductValidator() { - RuleFor(x => x.Name) + RuleFor(x => x.Body.Name) .NotEmpty() .MinimumLength(3); - RuleFor(x => x.Quantity) + RuleFor(x => x.Body.Quantity) .NotNull() .DependentRules(() => { - RuleFor(x => x.Quantity.Amount) + RuleFor(x => x.Body.Quantity.Amount) .GreaterThan(0); - RuleFor(x => x.Quantity.Unit) + RuleFor(x => x.Body.Quantity.Unit) .NotEmpty(); }); - RuleFor(x => x.BrandId) + RuleFor(x => x.Body.BrandId) .NotEmpty(); - RuleFor(x => x.CountryId) + RuleFor(x => x.Body.CountryId) .NotEmpty(); - RuleFor(x => x.CategoryId) + RuleFor(x => x.Body.CategoryId) .NotEmpty(); } } diff --git a/src/Products/Products.Core/Features/Products/Commands/DeleteProduct.cs b/src/Products/Products.Core/Features/Products/Commands/DeleteProduct.cs new file mode 100644 index 0000000..b0b8be4 --- /dev/null +++ b/src/Products/Products.Core/Features/Products/Commands/DeleteProduct.cs @@ -0,0 +1,48 @@ +using IGroceryStore.Products.Exceptions; +using IGroceryStore.Products.Persistence.Contexts; +using IGroceryStore.Shared.Abstraction.Commands; +using IGroceryStore.Shared.Abstraction.Common; +using IGroceryStore.Shared.Abstraction.Constants; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; + +namespace IGroceryStore.Products.Core.Features.Products.Commands; + +internal record DeleteProduct(ulong Id) : IHttpCommand; + +public class DeleteProductEndpoint : IEndpoint +{ + public void RegisterEndpoint(IEndpointRouteBuilder endpoints) => + endpoints.MapDelete("api/products/{id}") + //.RequireAuthorization() + .WithTags(SwaggerTags.Products) + .Produces(204) + .Produces(400); + +} + +internal class DeleteProductHandler : ICommandHandler +{ + private readonly ProductsDbContext _productsDbContext; + + public DeleteProductHandler(ProductsDbContext productsDbContext) + { + _productsDbContext = productsDbContext; + } + + public async Task HandleAsync(DeleteProduct command, CancellationToken cancellationToken = default) + { + var products = await _productsDbContext.Products.ToListAsync(); + + var product = + await _productsDbContext.Products.FirstOrDefaultAsync(x => x.Id.Equals(command.Id), cancellationToken); + + if (product is null) throw new ProductNotFoundException(command.Id); + + _productsDbContext.Products.Remove(product); + await _productsDbContext.SaveChangesAsync(cancellationToken); + + return Results.NoContent(); + } +} diff --git a/src/Products/Products.Core/Features/Products/Commands/UpdateDetails.cs b/src/Products/Products.Core/Features/Products/Commands/UpdateDetails.cs index c5442fd..4fe1a81 100644 --- a/src/Products/Products.Core/Features/Products/Commands/UpdateDetails.cs +++ b/src/Products/Products.Core/Features/Products/Commands/UpdateDetails.cs @@ -1,5 +1,58 @@ -namespace IGroceryStore.Products.Features.Products.Commands; +using IGroceryStore.Products.Contracts.Events; +using IGroceryStore.Products.Exceptions; +using IGroceryStore.Products.Persistence.Contexts; +using IGroceryStore.Shared.Abstraction.Commands; +using IGroceryStore.Shared.Abstraction.Common; +using IGroceryStore.Shared.Abstraction.Constants; +using MassTransit; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; -internal class UpdateDetails +namespace IGroceryStore.Products.Features.Products.Commands; + +internal record UpdateDetails(UpdateDetails.UpdateDetailsBody DetailsBody, ulong Id) : IHttpCommand +{ + internal record UpdateDetailsBody(string Name, + string Description + ); +} + +public class UpdateDetailsEndpoint : IEndpoint +{ + public void RegisterEndpoint(IEndpointRouteBuilder endpoints) + => endpoints.MapPut("api/products") + .WithTags(SwaggerTags.Products) + .Produces(204) + .Produces(400); +} + +internal class UpdateDetailsHandler : ICommandHandler { + private readonly ProductsDbContext _context; + private readonly IBus _bus; + + public UpdateDetailsHandler(ProductsDbContext context, IBus bus) + { + _context = context; + _bus = bus; + } + + public async Task HandleAsync(UpdateDetails command, CancellationToken cancellationToken = default) + { + var product = await _context.Products.FirstOrDefaultAsync(x => x.Id.Equals(command.Id), cancellationToken); + + if (product is null) throw new ProductNotFoundException(command.Id); + + var (name, description) = command.DetailsBody; + + product.Name = name; + product.Description = description; + _context.Update(product); + await _context.SaveChangesAsync(cancellationToken); + + await _bus.Publish(new ProductUpdated(command.Id, name), cancellationToken); + + return Results.NoContent(); + } } diff --git a/src/Products/Products.Core/Products.Core.csproj b/src/Products/Products.Core/Products.Core.csproj index cbe58e8..a06148c 100644 --- a/src/Products/Products.Core/Products.Core.csproj +++ b/src/Products/Products.Core/Products.Core.csproj @@ -10,18 +10,22 @@ - - + + - - + + - - + + + + + + diff --git a/src/Products/Products.Core/modulesettings.json b/src/Products/Products.Core/modulesettings.json index 91759e0..e6ecabb 100644 --- a/src/Products/Products.Core/modulesettings.json +++ b/src/Products/Products.Core/modulesettings.json @@ -1,5 +1,5 @@ { "Products": { - "ModuleEnabled": false + "ModuleEnabled": true } } \ No newline at end of file diff --git a/src/Users/Users.Core/Users.Core.csproj b/src/Users/Users.Core/Users.Core.csproj index 32a309c..9c901bb 100644 --- a/src/Users/Users.Core/Users.Core.csproj +++ b/src/Users/Users.Core/Users.Core.csproj @@ -10,22 +10,22 @@ - - + + - - + + - - - - - - + + + + + + diff --git a/tests/Products/Products.IntegrationTests/DbConfigExtensions.cs b/tests/Products/Products.IntegrationTests/DbConfigExtensions.cs new file mode 100644 index 0000000..93fa395 --- /dev/null +++ b/tests/Products/Products.IntegrationTests/DbConfigExtensions.cs @@ -0,0 +1,24 @@ +using DotNet.Testcontainers.Containers; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Products.IntegrationTests; + +public static class DbConfigExtensions +{ + public static void CleanDbContextOptions(this IServiceCollection services) + where T : DbContext + { + var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions)); + if (descriptor != null) services.Remove(descriptor); + services.RemoveAll(typeof(T)); + } + + public static void AddPostgresContext(this IServiceCollection services, TestcontainerDatabase dbContainer) + where T : DbContext + { + services.AddDbContext(ctx => + ctx.UseNpgsql(dbContainer.ConnectionString)); + } +} diff --git a/tests/Products/Products.IntegrationTests/ProductApiFactory.cs b/tests/Products/Products.IntegrationTests/ProductApiFactory.cs new file mode 100644 index 0000000..26b4437 --- /dev/null +++ b/tests/Products/Products.IntegrationTests/ProductApiFactory.cs @@ -0,0 +1,103 @@ +using Bogus; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Configurations; +using DotNet.Testcontainers.Containers; +using IGroceryStore.API; +using IGroceryStore.Products.Contracts.Events; +using IGroceryStore.Products.Persistence.Contexts; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; +using MassTransit; +using Microsoft.AspNetCore.TestHost; +using IGroceryStore.Shared.Tests.Auth; +using Respawn; +using System.Data.Common; +using IGroceryStore.Shared.Abstraction.Constants; +using System.Security.Claims; +using IGroceryStore.Shared.Services; +using Microsoft.Extensions.DependencyInjection; +using Npgsql; + +namespace Products.IntegrationTests; + +public class ProductApiFactory : WebApplicationFactory, IAsyncLifetime +{ + private readonly MockUser _user; + private Respawner _respawner = default!; + private DbConnection _dbConnection = default!; + private readonly TestcontainerDatabase _dbContainer = + new TestcontainersBuilder() + .WithDatabase(new PostgreSqlTestcontainerConfiguration + { + Database = Guid.NewGuid().ToString(), + Username = "postgres", + Password = "postgres" + }) + .WithAutoRemove(true) + .WithCleanUp(true) + .Build(); + + public ProductApiFactory() + { + _user = new MockUser(new Claim(Claims.Name.UserId, "1"), + new Claim(Claims.Name.Expire, DateTimeOffset.UtcNow.AddSeconds(2137).ToUnixTimeSeconds().ToString())); + Randomizer.Seed = new Random(420); + VerifierSettings.ScrubInlineGuids(); + } + + public HttpClient HttpClient { get; private set; } = default!; + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureLogging(logging => { logging.ClearProviders(); }); + + builder.UseEnvironment(EnvironmentService.TestEnvironment); + + builder.ConfigureServices(services => + { + services.AddMassTransitTestHarness(x => + { + x.AddHandler(context => context.ConsumeCompleted); + }); + }); + + builder.ConfigureTestServices(services => + { + //services.CleanDbContextOptions(); + services.CleanDbContextOptions(); + + //services.AddPostgresContext(_dbContainer); + services.AddPostgresContext(_dbContainer); + + services.AddTestAuthentication(); + + services.AddSingleton(_ => _user); + }); + } + + public async Task InitializeAsync() + { + await _dbContainer.StartAsync(); + _dbConnection = new NpgsqlConnection(_dbContainer.ConnectionString); + HttpClient = CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); + await InitializeRespawner(); + } + + public async Task ResetDatabaseAsync() => await _respawner.ResetAsync(_dbConnection); + + private async Task InitializeRespawner() + { + await _dbConnection.OpenAsync(); + _respawner = await Respawner.CreateAsync(_dbConnection, new RespawnerOptions() + { + DbAdapter = DbAdapter.Postgres, + SchemasToInclude = new[] { "IGroceryStore.Products" }, + }); + } + + async Task IAsyncLifetime.DisposeAsync() + { + await _dbContainer.DisposeAsync(); + } +} diff --git a/tests/Products/Products.IntegrationTests/ProductCollection.cs b/tests/Products/Products.IntegrationTests/ProductCollection.cs new file mode 100644 index 0000000..7668ac8 --- /dev/null +++ b/tests/Products/Products.IntegrationTests/ProductCollection.cs @@ -0,0 +1,8 @@ +using Products.IntegrationTests; + +namespace IGroceryStore.Products.IntegrationTests; + +[CollectionDefinition("ProductCollection")] +public class ProductCollection : ICollectionFixture +{ +} diff --git a/tests/Products/Products.IntegrationTests/Products.IntegrationTests.csproj b/tests/Products/Products.IntegrationTests/Products.IntegrationTests.csproj index 3ce04a7..bca1b3d 100644 --- a/tests/Products/Products.IntegrationTests/Products.IntegrationTests.csproj +++ b/tests/Products/Products.IntegrationTests/Products.IntegrationTests.csproj @@ -1,4 +1,4 @@ - + net7.0 @@ -10,6 +10,23 @@ IGroceryStore.Products.IntegrationTests + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + diff --git a/tests/Products/Products.IntegrationTests/Products/CreateProductTests.cs b/tests/Products/Products.IntegrationTests/Products/CreateProductTests.cs new file mode 100644 index 0000000..bd50881 --- /dev/null +++ b/tests/Products/Products.IntegrationTests/Products/CreateProductTests.cs @@ -0,0 +1,106 @@ +using System.Net; +using System.Net.Http.Json; +using System.Security.Claims; +using Bogus; +using FluentAssertions; +using IGroceryStore.Products.Features.Products.Commands; +using IGroceryStore.Products.IntegrationTests.TestModels; +using IGroceryStore.Products.ReadModels; +using IGroceryStore.Products.ValueObjects; +using IGroceryStore.Shared.Abstraction.Constants; +using IGroceryStore.Shared.Tests.Auth; +using Microsoft.AspNetCore.TestHost; +using Products.IntegrationTests; + +namespace IGroceryStore.Products.IntegrationTests.Products; + +[UsesVerify] +[Collection("ProductCollection")] +public class CreateProductTests : IClassFixture, IAsyncLifetime +{ + private readonly HttpClient _client; + private readonly Func _resetDatabase; + private readonly ProductsFakeSeeder _productsFakeSeeder; + + public CreateProductTests(ProductApiFactory productApiFactory) + { + _client = productApiFactory.HttpClient; + _resetDatabase = productApiFactory.ResetDatabaseAsync; + productApiFactory + .WithWebHostBuilder(builder => + builder.ConfigureTestServices(services => + { + services.RegisterUser(new[] + { + new Claim(Claims.Name.UserId, "1"), + new Claim(Claims.Name.Expire, + DateTimeOffset.UtcNow.AddSeconds(2137).ToUnixTimeSeconds().ToString()) + }); + })); // override authorized user; + + _productsFakeSeeder = new ProductsFakeSeeder(productApiFactory); + //productApiFactory.SeedProductDb(); + } + + [Fact] + public async Task CreateProduct_WhenDataIsValid() + { + // Arrange + await _productsFakeSeeder.SeedData(); + var createProduct = GetFakeCreateProduct(); + + // Act + var response = await _client.PostAsJsonAsync($"api/products", createProduct); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Accepted); + } + + [Fact] + public async Task CreateProduct_WhenCategoryNotExists_ShouldReturnNotFound() + { + // Arrange + await _productsFakeSeeder.SeedData(); + var createProduct = GetFakeCreateProduct(); + + await _client.DeleteAsync($"api/categories/{createProduct.CategoryId}"); + + // Act + var response = await _client.PostAsJsonAsync($"api/products", createProduct); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + + private CreateProduct.CreateProductBody GetFakeCreateProduct() + { + var units = new[] { Unit.Centimeter, Unit.Gram, Unit.Milliliter, Unit.Piece }; + + var brandsIds = TestBrands.Brands + .Select(x => x.Id.Id) + .ToList(); + + var countriesIds = TestCountries.Countries + .Select(x => x.Id.Id) + .ToList(); + + var categoriesIds = TestCategories.Categories + .Select(x => x.Id.Id) + .ToList(); + + var faker = new Faker(); + + var body = new CreateProduct.CreateProductBody( + faker.Commerce.ProductName(), + new QuantityReadModel(faker.Random.UInt(1, 20) * 100, faker.PickRandom(units)), + faker.PickRandom(brandsIds), + faker.PickRandom(countriesIds), + faker.PickRandom(categoriesIds) + ); + + return body; + } + + public Task InitializeAsync() => Task.CompletedTask; + public Task DisposeAsync() => _resetDatabase(); +} diff --git a/tests/Products/Products.IntegrationTests/Products/DeleteProductsTests.cs b/tests/Products/Products.IntegrationTests/Products/DeleteProductsTests.cs new file mode 100644 index 0000000..81791e5 --- /dev/null +++ b/tests/Products/Products.IntegrationTests/Products/DeleteProductsTests.cs @@ -0,0 +1,68 @@ +using System.Net; +using FluentAssertions; +using Microsoft.AspNetCore.TestHost; +using IGroceryStore.Shared.Tests.Auth; +using System.Security.Claims; +using IGroceryStore.Shared.Abstraction.Constants; +using IGroceryStore.Products.IntegrationTests; +using IGroceryStore.Products.IntegrationTests.TestModels; + +namespace Products.IntegrationTests.Products; + +[UsesVerify] +[Collection("ProductCollection")] +public class DeleteProductsTests : IClassFixture, IAsyncLifetime +{ + private readonly HttpClient _client; + private readonly Func _resetDatabase; + private readonly ProductsFakeSeeder _productsFakeSeeder; + + public DeleteProductsTests(ProductApiFactory productApiFactory) + { + _client = productApiFactory.HttpClient; + _resetDatabase = productApiFactory.ResetDatabaseAsync; + productApiFactory + .WithWebHostBuilder(builder => + builder.ConfigureTestServices(services => + { + services.RegisterUser(new[] + { + new Claim(Claims.Name.UserId, "1"), + new Claim(Claims.Name.Expire, + DateTimeOffset.UtcNow.AddSeconds(2137).ToUnixTimeSeconds().ToString()) + }); + })); // override authorized user; + + _productsFakeSeeder = new ProductsFakeSeeder(productApiFactory); + } + + [Fact] + public async Task DeleteProduct_WhenProductExists() + { + // Arrange + await _productsFakeSeeder.SeedData(); + var product = TestProducts.Product; + + // Act + var response = await _client.DeleteAsync($"api/products/{product.Id.Value}"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NoContent); + } + + [Fact] + public async Task ReturnNotFound_WhenProductNotExists() + { + // Arrange + var product = TestProducts.Product; + + // Act + var response = await _client.DeleteAsync($"api/products/{product.Id.Value}"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + + public Task InitializeAsync() => Task.CompletedTask; + public Task DisposeAsync() => _resetDatabase(); +} diff --git a/tests/Products/Products.IntegrationTests/ProductsFakeSeeder.cs b/tests/Products/Products.IntegrationTests/ProductsFakeSeeder.cs new file mode 100644 index 0000000..471f98d --- /dev/null +++ b/tests/Products/Products.IntegrationTests/ProductsFakeSeeder.cs @@ -0,0 +1,30 @@ +using IGroceryStore.Products.IntegrationTests.TestModels; +using IGroceryStore.Products.Persistence.Contexts; +using Microsoft.Extensions.DependencyInjection; +using Products.IntegrationTests; + +namespace IGroceryStore.Products.IntegrationTests; + +public class ProductsFakeSeeder +{ + private ProductApiFactory _productApiFactory; + + public ProductsFakeSeeder(ProductApiFactory productApiFactory) + { + _productApiFactory = productApiFactory; + } + + public async Task SeedData() + { + //await _productApiFactory.ResetDatabaseAsync(); + + using var scope = _productApiFactory.Services.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + await context.Countries.AddRangeAsync(TestCountries.Countries); + await context.Brands.AddRangeAsync(TestBrands.Brands); + await context.Categories.AddRangeAsync(TestCategories.Categories); + await context.Products.AddRangeAsync(TestProducts.Products); + await context.SaveChangesAsync(); + } +} diff --git a/tests/Products/Products.IntegrationTests/TestModels/TestBrands.cs b/tests/Products/Products.IntegrationTests/TestModels/TestBrands.cs new file mode 100644 index 0000000..cf6483d --- /dev/null +++ b/tests/Products/Products.IntegrationTests/TestModels/TestBrands.cs @@ -0,0 +1,28 @@ +using Bogus; +using IGroceryStore.Products.Entities; +using IGroceryStore.Products.ValueObjects; + +namespace IGroceryStore.Products.IntegrationTests.TestModels; + +internal static class TestBrands +{ + private static readonly Faker ProductGenerator = new BrandFaker(); + + public static readonly List Brands = ProductGenerator.Generate(10); + public static readonly Brand Brand = Brands.First(); + + private sealed class BrandFaker : Faker + { + public BrandFaker() + { + CustomInstantiator(ResolveConstructor); + } + + private Brand ResolveConstructor(Faker faker) + { + var brand = new Brand { Id = new BrandId((ulong)faker.UniqueIndex), Name = faker.Company.CompanyName() }; + + return brand; + } + } +} diff --git a/tests/Products/Products.IntegrationTests/TestModels/TestCategories.cs b/tests/Products/Products.IntegrationTests/TestModels/TestCategories.cs new file mode 100644 index 0000000..5652d62 --- /dev/null +++ b/tests/Products/Products.IntegrationTests/TestModels/TestCategories.cs @@ -0,0 +1,31 @@ +using Bogus; +using IGroceryStore.Products.Entities; +using IGroceryStore.Products.Features.Categories.Commands; +using IGroceryStore.Products.ValueObjects; + +namespace IGroceryStore.Products.IntegrationTests.TestModels; + +internal static class TestCategories +{ + private static readonly Faker CategoryGenerator = new CategoryFaker(); + + public static readonly List Categories = CategoryGenerator.Generate(10); + public static readonly Category Category = Categories.First(); + + private sealed class CategoryFaker : Faker + { + public CategoryFaker() + { + CustomInstantiator(ResolveConstructor); + } + + private Category ResolveConstructor(Faker faker) + { + return new Category + { + Id = new CategoryId((ulong)faker.UniqueIndex), + Name = faker.Commerce.Categories(1).First() + }; + } + } +} diff --git a/tests/Products/Products.IntegrationTests/TestModels/TestCountries.cs b/tests/Products/Products.IntegrationTests/TestModels/TestCountries.cs new file mode 100644 index 0000000..2f49073 --- /dev/null +++ b/tests/Products/Products.IntegrationTests/TestModels/TestCountries.cs @@ -0,0 +1,31 @@ +using Bogus; +using IGroceryStore.Products.Entities; +using IGroceryStore.Products.ValueObjects; + +namespace IGroceryStore.Products.IntegrationTests.TestModels; + +internal static class TestCountries +{ + private static readonly Faker CountryGenerator = new CountryFaker(); + + public static readonly List Countries = CountryGenerator.Generate(10); + public static readonly Country Country = Countries.First(); + + private sealed class CountryFaker : Faker + { + public CountryFaker() + { + CustomInstantiator(ResolveConstructor); + } + + private Country ResolveConstructor(Faker faker) + { + return new Country + { + Id = new CountryId((ulong)faker.UniqueIndex), + Name = faker.Address.Country(), + Code = faker.Address.CountryCode() + }; + } + } +} diff --git a/tests/Products/Products.IntegrationTests/TestModels/TestProducts.cs b/tests/Products/Products.IntegrationTests/TestModels/TestProducts.cs new file mode 100644 index 0000000..f2d1c6f --- /dev/null +++ b/tests/Products/Products.IntegrationTests/TestModels/TestProducts.cs @@ -0,0 +1,40 @@ +using Bogus; +using IGroceryStore.Products.Entities; +using IGroceryStore.Products.ValueObjects; +using IGroceryStore.Shared.ValueObjects; + +namespace IGroceryStore.Products.IntegrationTests.TestModels; + +internal static class TestProducts +{ + private static readonly Faker CreateProductGenerator = new ProductFaker(); + + public static readonly List Products = CreateProductGenerator.Generate(10); + public static readonly Product Product = Products.First(); + + private sealed class ProductFaker : Faker + { + public ProductFaker() + { + CustomInstantiator(ResolveConstructor); + } + + private Product ResolveConstructor(Faker faker) + { + var units = new[] { Unit.Centimeter, Unit.Gram, Unit.Milliliter, Unit.Piece }; + + return new Product + { + Id = new ProductId(faker.Random.UInt()), + Name = new ProductName(faker.Commerce.ProductName()), + Description = new Description(faker.Commerce.ProductDescription()), + Quantity = new Quantity(faker.Random.UInt(1, 20) * 100, faker.PickRandom(units)), + CountryId = new CountryId(TestCountries.Country.Id), + CategoryId = new CategoryId(TestCategories.Category.Id), + BrandId = new BrandId(TestBrands.Brand.Id), + ImageUrl = new Uri(faker.Internet.Url()), + BarCode = new BarCode(faker.Random.UInt(100_000_000, 900_000_000).ToString()) + }; + } + } +}