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 <tests>" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
- <Project Location="/Users/adrianfranczak/Repos/Private/IGroceryStore" Presentation="<tests>" />
+
+ <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from <tests>" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
+ <Project Location="\Users\adrianfranczak\Repos\Private\IGroceryStore" Presentation="<tests>" />
</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())
+ };
+ }
+ }
+}