feat: add integration tests
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
|
||||
<PropertyGroup Label="Build">
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<!-- Disable overriding package versions -->
|
||||
<!-- https://devblogs.microsoft.com/nuget/introducing-central-package-management/#overriding-package-versions -->
|
||||
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -22,6 +24,7 @@
|
||||
<PackageVersion Include="FluentValidation" Version="11.7.1" />
|
||||
<PackageVersion Include="MediatR" Version="12.1.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.9" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="5.1.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.1.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
||||
@@ -58,7 +61,7 @@
|
||||
<PackageVersion Include="NSubstitute.Analyzers.CSharp" Version="1.0.16" />
|
||||
<PackageVersion Include="NSubstitute" Version="5.0.0" />
|
||||
<PackageVersion Include="Polly" Version="7.2.4" />
|
||||
<PackageVersion Include="Roslynator.Analyzers" Version="4.4.0" />
|
||||
<PackageVersion Include="Roslynator.Analyzers" Version="4.5.0" />
|
||||
<PackageVersion Include="SonarAnalyzer.CSharp" Version="9.8.0.76515" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="7.0.0" />
|
||||
|
||||
@@ -28,7 +28,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
README.md = README.md
|
||||
global.json = global.json
|
||||
Directory.Packages.props = Directory.Packages.props
|
||||
requests.http = requests.http
|
||||
requests.http = requests.http
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shop.Query", "src\Shop.Query\Shop.Query.csproj", "{C2A82AAD-0348-4B29-B7E7-E36509EE068C}"
|
||||
@@ -48,6 +48,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
|
||||
.github\workflows\sonar-cloud.yml = .github\workflows\sonar-cloud.yml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shop.IntegrationTests", "tests\Shop.IntegrationTests\Shop.IntegrationTests.csproj", "{7BA06288-8446-4099-9341-D213A835763C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -85,6 +87,10 @@ Global
|
||||
{C2A82AAD-0348-4B29-B7E7-E36509EE068C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C2A82AAD-0348-4B29-B7E7-E36509EE068C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C2A82AAD-0348-4B29-B7E7-E36509EE068C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7BA06288-8446-4099-9341-D213A835763C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7BA06288-8446-4099-9341-D213A835763C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7BA06288-8446-4099-9341-D213A835763C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7BA06288-8446-4099-9341-D213A835763C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{D957E297-38EA-402F-B603-25F3EC156E18} = {DFB189FE-F11B-46EA-A5CD-3571CEC8193A}
|
||||
@@ -96,5 +102,6 @@ Global
|
||||
{C2A82AAD-0348-4B29-B7E7-E36509EE068C} = {8011CDDD-0814-4C74-9B74-C3E3365D50CC}
|
||||
{E94C4296-DC64-4C22-87A1-B5A054FAADAC} = {8011CDDD-0814-4C74-9B74-C3E3365D50CC}
|
||||
{11296DB2-D948-4ADF-A4CE-3F2BFF627478} = {8011CDDD-0814-4C74-9B74-C3E3365D50CC}
|
||||
{7BA06288-8446-4099-9341-D213A835763C} = {DFB189FE-F11B-46EA-A5CD-3571CEC8193A}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -20,5 +20,5 @@ public sealed class ConnectionOptions : IAppOptions
|
||||
[Required]
|
||||
public string CacheConnection { get; private init; }
|
||||
|
||||
public bool CacheConnectionInMemory() => SqlConnection.Equals("InMemory", ComparisonType);
|
||||
public bool CacheConnectionInMemory() => CacheConnection.Equals("InMemory", ComparisonType);
|
||||
}
|
||||
@@ -16,7 +16,7 @@ public class WriteDbContext : BaseDbContext<WriteDbContext>
|
||||
public WriteDbContext(DbContextOptions<WriteDbContext> dbOptions) : base(dbOptions) =>
|
||||
_encryptionProvider = new AesProvider(EncryptionKey, EncryptionIv);
|
||||
|
||||
internal DbSet<Customer> Customers => Set<Customer>();
|
||||
public DbSet<Customer> Customers => Set<Customer>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
|
||||
@@ -15,19 +15,16 @@ internal class CustomerConfiguration : IEntityTypeConfiguration<Customer>
|
||||
builder
|
||||
.Property(customer => customer.FirstName)
|
||||
.IsRequired() // NOT NULL
|
||||
.IsUnicode(false) // VARCHAR
|
||||
.HasMaxLength(100);
|
||||
|
||||
builder
|
||||
.Property(customer => customer.LastName)
|
||||
.IsRequired() // NOT NULL
|
||||
.IsUnicode(false) // VARCHAR
|
||||
.HasMaxLength(100);
|
||||
|
||||
builder
|
||||
.Property(customer => customer.Gender)
|
||||
.IsRequired() // NOT NULL
|
||||
.IsUnicode(false) // VARCHAR
|
||||
.HasMaxLength(6)
|
||||
.HasConversion<string>();
|
||||
|
||||
@@ -37,7 +34,6 @@ internal class CustomerConfiguration : IEntityTypeConfiguration<Customer>
|
||||
ownedNav
|
||||
.Property(email => email.Address)
|
||||
.IsRequired() // NOT NULL
|
||||
.IsUnicode(false) // VARCHAR
|
||||
.HasMaxLength(254)
|
||||
.HasColumnName(nameof(Customer.Email))
|
||||
.IsEncrypted(); // Encrypted Column, ref: https://github.com/Eastrall/EntityFrameworkCore.DataEncryption
|
||||
|
||||
@@ -23,14 +23,11 @@ internal class EventStoreConfiguration : IEntityTypeConfiguration<EventStore>
|
||||
builder
|
||||
.Property(eventStore => eventStore.MessageType)
|
||||
.IsRequired() // NOT NULL
|
||||
.IsUnicode(false) // VARCHAR
|
||||
.HasMaxLength(100);
|
||||
|
||||
builder
|
||||
.Property(eventStore => eventStore.Data)
|
||||
.IsRequired() // NOT NULL
|
||||
.IsUnicode(false) // VARCHAR
|
||||
.HasColumnType("VARCHAR(MAX)")
|
||||
.HasComment("JSON serialized event");
|
||||
|
||||
builder
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ using Shop.PublicApi.Models;
|
||||
using Shop.Query.Application.Customer.Queries;
|
||||
using Shop.Query.QueriesModel;
|
||||
|
||||
namespace Shop.PublicApi.Controllers;
|
||||
namespace Shop.PublicApi.Controllers.V1;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
@@ -3,7 +3,7 @@ using Shop.PublicApi.Middlewares;
|
||||
|
||||
namespace Shop.PublicApi.Extensions;
|
||||
|
||||
public static class MiddlewareExtensions
|
||||
internal static class MiddlewareExtensions
|
||||
{
|
||||
public static void UseCorrelationId(this IApplicationBuilder builder) =>
|
||||
builder.UseMiddleware<CorrelationIdMiddleware>();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -12,7 +13,35 @@ namespace Shop.PublicApi.Extensions;
|
||||
|
||||
internal static class WebApplicationExtensions
|
||||
{
|
||||
public static async Task MigrateDbAsync(this WebApplication app, AsyncServiceScope serviceScope)
|
||||
public static async Task RunAppAsync(this WebApplication app)
|
||||
{
|
||||
await using var serviceScope = app.Services.CreateAsyncScope();
|
||||
|
||||
var mapper = serviceScope.ServiceProvider.GetRequiredService<IMapper>();
|
||||
|
||||
app.Logger.LogInformation("----- AutoMapper: mappings are being validated...");
|
||||
|
||||
// Validate the AutoMapper configuration by asserting that the mappings are valid
|
||||
mapper.ConfigurationProvider.AssertConfigurationIsValid();
|
||||
|
||||
// Compile the AutoMapper mappings for better performance
|
||||
mapper.ConfigurationProvider.CompileMappings();
|
||||
|
||||
app.Logger.LogInformation("----- AutoMapper: mappings are valid!");
|
||||
|
||||
app.Logger.LogInformation("----- Databases are being migrated....");
|
||||
|
||||
// Migrate the databases asynchronously using the provided service scope
|
||||
await app.MigrateDataBasesAsync(serviceScope);
|
||||
|
||||
app.Logger.LogInformation("----- Databases have been successfully migrated!");
|
||||
|
||||
app.Logger.LogInformation("----- Application is starting....");
|
||||
|
||||
await app.RunAsync();
|
||||
}
|
||||
|
||||
private static async Task MigrateDataBasesAsync(this WebApplication app, AsyncServiceScope serviceScope)
|
||||
{
|
||||
await using var writeDbContext = serviceScope.ServiceProvider.GetRequiredService<WriteDbContext>();
|
||||
await using var eventStoreDbContext = serviceScope.ServiceProvider.GetRequiredService<EventStoreDbContext>();
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Shop.PublicApi.Models;
|
||||
|
||||
public sealed class ApiErrorResponse
|
||||
{
|
||||
[JsonConstructor]
|
||||
public ApiErrorResponse(string message) => Message = message;
|
||||
|
||||
public string Message { get; }
|
||||
@@ -15,9 +17,22 @@ public sealed class ApiErrorResponse
|
||||
|
||||
public class ApiResponse
|
||||
{
|
||||
public bool Success { get; protected init; }
|
||||
public string SuccessMessage { get; protected init; }
|
||||
public int StatusCode { get; protected init; }
|
||||
[JsonConstructor]
|
||||
public ApiResponse(bool success, string successMessage, int statusCode, IEnumerable<ApiErrorResponse> errors)
|
||||
{
|
||||
Success = success;
|
||||
SuccessMessage = successMessage;
|
||||
StatusCode = statusCode;
|
||||
Errors = errors;
|
||||
}
|
||||
|
||||
public ApiResponse()
|
||||
{
|
||||
}
|
||||
|
||||
public bool Success { get; protected set; }
|
||||
public string SuccessMessage { get; protected set; }
|
||||
public int StatusCode { get; protected set; }
|
||||
public IEnumerable<ApiErrorResponse> Errors { get; private init; } = Enumerable.Empty<ApiErrorResponse>();
|
||||
|
||||
public static ApiResponse Ok() =>
|
||||
@@ -70,10 +85,24 @@ public class ApiResponse
|
||||
|
||||
private static IEnumerable<ApiErrorResponse> CreateErrors(string errorMessage) =>
|
||||
new[] { new ApiErrorResponse(errorMessage) };
|
||||
|
||||
public override string ToString() =>
|
||||
$"Success: {Success} | StatusCode: {StatusCode} | HasErrors: {Errors.Any()}";
|
||||
}
|
||||
|
||||
public sealed class ApiResponse<T> : ApiResponse
|
||||
{
|
||||
[JsonConstructor]
|
||||
public ApiResponse(T result, bool success, string successMessage, int statusCode, IEnumerable<ApiErrorResponse> errors)
|
||||
: base(success, successMessage, statusCode, errors)
|
||||
{
|
||||
Result = result;
|
||||
}
|
||||
|
||||
public ApiResponse()
|
||||
{
|
||||
}
|
||||
|
||||
public T Result { get; private init; }
|
||||
|
||||
public static ApiResponse<T> Ok(T result) =>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Globalization;
|
||||
using System.IO.Compression;
|
||||
using AutoMapper;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Resources;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
@@ -10,7 +9,6 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.ResponseCompression;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -24,8 +22,7 @@ using StackExchange.Profiling;
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services
|
||||
.Configure<KestrelServerOptions>(kestrelOptions => kestrelOptions.AddServerHeader = false)
|
||||
.Configure<GzipCompressionProviderOptions>(compressionOptions => compressionOptions.Level = CompressionLevel.Optimal)
|
||||
.Configure<GzipCompressionProviderOptions>(compressionOptions => compressionOptions.Level = CompressionLevel.Fastest)
|
||||
.Configure<JsonOptions>(jsonOptions => jsonOptions.JsonSerializerOptions.Configure())
|
||||
.Configure<RouteOptions>(routeOptions => routeOptions.LowercaseUrls = true);
|
||||
|
||||
@@ -134,24 +131,8 @@ app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
|
||||
await using var serviceScope = app.Services.CreateAsyncScope();
|
||||
var mapper = serviceScope.ServiceProvider.GetRequiredService<IMapper>();
|
||||
await app.RunAppAsync();
|
||||
|
||||
app.Logger.LogInformation("----- AutoMapper: mappings are being validated...");
|
||||
|
||||
// Dry run all configured type maps and throw AutoMapper.AutoMapperConfigurationException for each problem.
|
||||
mapper.ConfigurationProvider.AssertConfigurationIsValid();
|
||||
|
||||
// Compile all underlying mapping expressions to cached delegates.
|
||||
mapper.ConfigurationProvider.CompileMappings();
|
||||
|
||||
app.Logger.LogInformation("----- AutoMapper: mappings are valid!");
|
||||
|
||||
app.Logger.LogInformation("----- Databases are being migrated....");
|
||||
|
||||
await app.MigrateDbAsync(serviceScope);
|
||||
|
||||
app.Logger.LogInformation("----- Databases have been successfully migrated!");
|
||||
|
||||
app.Logger.LogInformation("----- Application is starting....");
|
||||
await app.RunAsync();
|
||||
#pragma warning disable S1118
|
||||
public partial class Program { }
|
||||
#pragma warning restore S1118
|
||||
@@ -58,8 +58,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Label="Unit Tests">
|
||||
<InternalsVisibleTo Include="Shop.UnitTests" />
|
||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||
<InternalsVisibleTo Include="Shop.IntegrationTests" />
|
||||
<InternalsVisibleTo Include="Shop.UnitTests" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -59,23 +59,30 @@ public static class ServicesCollectionExtensions
|
||||
/// </summary>
|
||||
private static void ConfigureMongoDb()
|
||||
{
|
||||
// Step 1: Configure the serializer for Guid type.
|
||||
BsonSerializer.RegisterSerializer(new GuidSerializer(GuidRepresentation.CSharpLegacy));
|
||||
try
|
||||
{
|
||||
// Step 1: Configure the serializer for Guid type.
|
||||
BsonSerializer.TryRegisterSerializer(new GuidSerializer(GuidRepresentation.CSharpLegacy));
|
||||
|
||||
// Step 2: Configure the conventions to be applied to all mappings.
|
||||
// REF: https://mongodb.github.io/mongo-csharp-driver/2.0/reference/bson/mapping/conventions/
|
||||
ConventionRegistry.Register("Conventions",
|
||||
new ConventionPack
|
||||
{
|
||||
new CamelCaseElementNameConvention(), // Convert element names to camel case
|
||||
new EnumRepresentationConvention(BsonType.String), // Serialize enums as strings
|
||||
new IgnoreExtraElementsConvention(true), // Ignore extra elements when deserializing
|
||||
new IgnoreIfNullConvention(true) // Ignore null values when serializing
|
||||
}, _ => true);
|
||||
// Step 2: Configure the conventions to be applied to all mappings.
|
||||
// REF: https://mongodb.github.io/mongo-csharp-driver/2.0/reference/bson/mapping/conventions/
|
||||
ConventionRegistry.Register("Conventions",
|
||||
new ConventionPack
|
||||
{
|
||||
new CamelCaseElementNameConvention(), // Convert element names to camel case
|
||||
new EnumRepresentationConvention(BsonType.String), // Serialize enums as strings
|
||||
new IgnoreExtraElementsConvention(true), // Ignore extra elements when deserializing
|
||||
new IgnoreIfNullConvention(true) // Ignore null values when serializing
|
||||
}, _ => true);
|
||||
|
||||
// Step 3: Register the mappings configurations.
|
||||
// It is recommended to register all mappings before initializing the connection with MongoDb
|
||||
// REF: https://mongodb.github.io/mongo-csharp-driver/2.0/reference/bson/mapping/
|
||||
new CustomerMap().Configure(); // Configuration for Customer class
|
||||
// Step 3: Register the mappings configurations.
|
||||
// It is recommended to register all mappings before initializing the connection with MongoDb
|
||||
// REF: https://mongodb.github.io/mongo-csharp-driver/2.0/reference/bson/mapping/
|
||||
new CustomerMap().Configure(); // Configuration for Customer class
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Bogus;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NSubstitute;
|
||||
using Shop.Application.Customer.Commands;
|
||||
using Shop.Application.Customer.Responses;
|
||||
using Shop.Core.Extensions;
|
||||
using Shop.Domain.Entities.CustomerAggregate;
|
||||
using Shop.Domain.ValueObjects;
|
||||
using Shop.Infrastructure.Data.Context;
|
||||
using Shop.PublicApi.Models;
|
||||
using Shop.Query.Abstractions;
|
||||
using Shop.Query.Data.Context;
|
||||
using Xunit;
|
||||
using Xunit.Categories;
|
||||
|
||||
namespace Shop.IntegrationTests.Controllers.V1;
|
||||
|
||||
[IntegrationTest]
|
||||
public class CustomersControllerTests : IAsyncLifetime
|
||||
{
|
||||
private const string ConnectionString = "Data Source=:memory:";
|
||||
private const string Endpoint = "/api/customers";
|
||||
private readonly SqliteConnection _eventStoreDbContextSqlite = new(ConnectionString);
|
||||
private readonly SqliteConnection _writeDbContextSqlite = new(ConnectionString);
|
||||
|
||||
[Fact]
|
||||
public async Task Should_ReturnsHttpStatus200Ok_When_Post_ValidRequest()
|
||||
{
|
||||
// Arrange
|
||||
await using var webApplicationFactory = InitializeWebAppFactory();
|
||||
using var httpClient = webApplicationFactory.CreateClient(CreateClientOptions());
|
||||
|
||||
var command = new Faker<CreateCustomerCommand>()
|
||||
.RuleFor(command => command.FirstName, faker => faker.Person.FirstName)
|
||||
.RuleFor(command => command.LastName, faker => faker.Person.LastName)
|
||||
.RuleFor(command => command.Email, faker => faker.Person.Email)
|
||||
.RuleFor(command => command.Gender, faker => faker.PickRandom<EGender>())
|
||||
.RuleFor(command => command.DateOfBirth, faker => faker.Person.DateOfBirth)
|
||||
.Generate();
|
||||
|
||||
var commandAsJsonString = command.ToJson();
|
||||
|
||||
// Act
|
||||
using var jsonContent = new StringContent(commandAsJsonString, Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||
using var act = await httpClient.PostAsync(Endpoint, jsonContent);
|
||||
|
||||
// Assert (Http)
|
||||
act.Should().NotBeNull();
|
||||
act.IsSuccessStatusCode.Should().BeTrue();
|
||||
act.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
// Assert (Http Content Response)
|
||||
var response = (await act.Content.ReadAsStringAsync()).FromJson<ApiResponse<CreatedCustomerResponse>>();
|
||||
response.Should().NotBeNull();
|
||||
response.Success.Should().BeTrue();
|
||||
response.StatusCode.Should().Be(StatusCodes.Status200OK);
|
||||
response.Result.Should().NotBeNull();
|
||||
response.Result.Id.Should().NotBeEmpty();
|
||||
response.Errors.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_ReturnsHttpStatus400BadRequest_When_Post_InvalidRequest()
|
||||
{
|
||||
// Arrange
|
||||
await using var webApplicationFactory = InitializeWebAppFactory();
|
||||
using var httpClient = webApplicationFactory.CreateClient(CreateClientOptions());
|
||||
|
||||
var command = new Faker<CreateCustomerCommand>().Generate();
|
||||
var commandAsJsonString = command.ToJson();
|
||||
|
||||
// Act
|
||||
using var jsonContent = new StringContent(commandAsJsonString, Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||
using var act = await httpClient.PostAsync(Endpoint, jsonContent);
|
||||
|
||||
// Assert (Http)
|
||||
act.Should().NotBeNull();
|
||||
act.IsSuccessStatusCode.Should().BeFalse();
|
||||
act.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
|
||||
// Assert (Http Content Response)
|
||||
var response = (await act.Content.ReadAsStringAsync()).FromJson<ApiResponse<CreatedCustomerResponse>>();
|
||||
response.Should().NotBeNull();
|
||||
response.Success.Should().BeFalse();
|
||||
response.StatusCode.Should().Be(StatusCodes.Status400BadRequest);
|
||||
response.Result.Should().BeNull();
|
||||
response.Errors.Should().NotBeNullOrEmpty().And.OnlyHaveUniqueItems();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_ReturnsHttpStatus400BadRequest_When_Post_EmailAddressIsAlready()
|
||||
{
|
||||
// Arrange
|
||||
var customer = new Faker<Customer>()
|
||||
.CustomInstantiator(faker =>
|
||||
{
|
||||
var emailResult = Email.Create(faker.Person.Email);
|
||||
return new Customer(
|
||||
faker.Person.FirstName,
|
||||
faker.Person.LastName,
|
||||
faker.PickRandom<EGender>(),
|
||||
emailResult.Value,
|
||||
faker.Person.DateOfBirth);
|
||||
})
|
||||
.Generate();
|
||||
|
||||
await using var webApplicationFactory = InitializeWebAppFactory((serviceScope) =>
|
||||
{
|
||||
var writeDbContext = serviceScope.ServiceProvider.GetRequiredService<WriteDbContext>();
|
||||
writeDbContext.Customers.Add(customer);
|
||||
writeDbContext.SaveChanges();
|
||||
});
|
||||
using var httpClient = webApplicationFactory.CreateClient(CreateClientOptions());
|
||||
|
||||
var command = new Faker<CreateCustomerCommand>()
|
||||
.RuleFor(command => command.FirstName, faker => faker.Person.FirstName)
|
||||
.RuleFor(command => command.LastName, faker => faker.Person.LastName)
|
||||
.RuleFor(command => command.Email, customer.Email.Address)
|
||||
.RuleFor(command => command.Gender, faker => faker.PickRandom<EGender>())
|
||||
.RuleFor(command => command.DateOfBirth, faker => faker.Person.DateOfBirth)
|
||||
.Generate();
|
||||
|
||||
var commandAsJsonString = command.ToJson();
|
||||
|
||||
// Act
|
||||
using var jsonContent = new StringContent(commandAsJsonString, Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||
using var act = await httpClient.PostAsync(Endpoint, jsonContent);
|
||||
|
||||
// Assert (Http)
|
||||
act.Should().NotBeNull();
|
||||
act.IsSuccessStatusCode.Should().BeFalse();
|
||||
act.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
|
||||
// Assert (Http Content Response)
|
||||
var response = (await act.Content.ReadAsStringAsync()).FromJson<ApiResponse<CreatedCustomerResponse>>();
|
||||
response.Should().NotBeNull();
|
||||
response.Success.Should().BeFalse();
|
||||
response.StatusCode.Should().Be(StatusCodes.Status400BadRequest);
|
||||
response.Result.Should().BeNull();
|
||||
response.Errors.Should().NotBeNullOrEmpty()
|
||||
.And.OnlyHaveUniqueItems()
|
||||
.And.AllSatisfy(error => error.Message.Should().Be("The provided email address is already in use."));
|
||||
}
|
||||
|
||||
#region IAsyncLifetime
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _writeDbContextSqlite.OpenAsync();
|
||||
await _eventStoreDbContextSqlite.OpenAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _writeDbContextSqlite.DisposeAsync();
|
||||
await _eventStoreDbContextSqlite.DisposeAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private WebApplicationFactory<Program> InitializeWebAppFactory(Action<IServiceScope> configureServices = null)
|
||||
{
|
||||
return new WebApplicationFactory<Program>()
|
||||
.WithWebHostBuilder(hostBuilder =>
|
||||
{
|
||||
hostBuilder.UseSetting("ConnectionStrings:SqlConnection", "InMemory");
|
||||
hostBuilder.UseSetting("ConnectionStrings:NoSqlConnection", "InMemory");
|
||||
hostBuilder.UseSetting("ConnectionStrings:CacheConnection", "InMemory");
|
||||
hostBuilder.UseSetting("CacheOptions:AbsoluteExpirationInHours", "1");
|
||||
hostBuilder.UseSetting("CacheOptions:SlidingExpirationInSeconds", "30");
|
||||
|
||||
hostBuilder.UseEnvironment(Environments.Production);
|
||||
|
||||
hostBuilder.ConfigureServices(services =>
|
||||
{
|
||||
services.RemoveAll<WriteDbContext>();
|
||||
services.RemoveAll<DbContextOptions<WriteDbContext>>();
|
||||
services.RemoveAll<EventStoreDbContext>();
|
||||
services.RemoveAll<DbContextOptions<EventStoreDbContext>>();
|
||||
services.RemoveAll<NoSqlDbContext>();
|
||||
services.RemoveAll<ISynchronizeDb>();
|
||||
|
||||
services.AddDbContext<WriteDbContext>(options => options.UseSqlite(_writeDbContextSqlite));
|
||||
services.AddDbContext<EventStoreDbContext>(options => options.UseSqlite(_eventStoreDbContextSqlite));
|
||||
services.AddSingleton(_ => Substitute.For<IReadDbContext>());
|
||||
services.AddSingleton(_ => Substitute.For<ISynchronizeDb>());
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider(true);
|
||||
using var serviceScope = serviceProvider.CreateScope();
|
||||
|
||||
var writeDbContext = serviceScope.ServiceProvider.GetRequiredService<WriteDbContext>();
|
||||
writeDbContext.Database.EnsureCreated();
|
||||
|
||||
var eventStoreDbContext = serviceScope.ServiceProvider.GetRequiredService<EventStoreDbContext>();
|
||||
eventStoreDbContext.Database.EnsureCreated();
|
||||
|
||||
configureServices?.Invoke(serviceScope);
|
||||
|
||||
writeDbContext.Dispose();
|
||||
eventStoreDbContext.Dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static WebApplicationFactoryClientOptions CreateClientOptions() =>
|
||||
new() { BaseAddress = new Uri("https://127.0.0.1"), AllowAutoRedirect = false };
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Ardalis.Result" />
|
||||
<PackageReference Include="AutoMapper" />
|
||||
<PackageReference Include="Bogus" />
|
||||
<PackageReference Include="coverlet.collector">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.msbuild">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="FluentValidation" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="NSubstitute" />
|
||||
<PackageReference Include="NSubstitute.Analyzers.CSharp">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.analyzers" />
|
||||
<PackageReference Include="xunit.categories" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Label="Project References">
|
||||
<ProjectReference Include="..\..\src\Shop.PublicApi\Shop.PublicApi.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="xunit.runner.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
||||
"diagnosticMessages": true,
|
||||
"parallelizeAssembly": true,
|
||||
"parallelizeTestCollections": true,
|
||||
"preEnumerateTheories": true
|
||||
}
|
||||
Reference in New Issue
Block a user