Merge pull request #33 from meysamhadeli/develop

move integration test base to building blocks
This commit is contained in:
Meysam Hadeli 2022-07-18 23:20:15 +04:30 committed by GitHub
commit 5d8077963a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 728 additions and 1318 deletions

View File

@ -341,6 +341,10 @@ dotnet_diagnostic.RCS1046.severity = Suggestion
# RCS1047: Non-asynchronous method name should not end with 'Async'.
dotnet_diagnostic.RCS1047.severity = error
# https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS1174.md
# RCS1174: Remove redundant async/await.
dotnet_diagnostic.RCS1174.severity = None
##################################################################################
## https://github.com/semihokur/asyncfixer
## AsyncFixer01

View File

@ -120,7 +120,8 @@
<PackageReference Include="FluentAssertions" Version="6.2.0" />
<PackageReference Include="MediatR" Version="9.0.0" />
<PackageReference Include="Respawn" Version="4.0.0" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="6.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.4" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />

View File

@ -61,6 +61,8 @@ namespace BuildingBlocks.Contracts.Grpc;
public FlightStatus Status { get; init; }
[Key(10)]
public decimal Price { get; init; }
[Key(11)]
public long FlightId { get; init; }
}
public enum FlightStatus

View File

@ -24,7 +24,6 @@ public abstract class AppDbContextBase : DbContext, IDbContext
protected override void OnModelCreating(ModelBuilder builder)
{
// ref: https://github.com/pdevito3/MessageBusTestingInMemHarness/blob/main/RecipeManagement/src/RecipeManagement/Databases/RecipesDbContext.cs
builder.FilterSoftDeletedProperties();
}
public async Task BeginTransactionAsync(CancellationToken cancellationToken = default)

View File

@ -1,10 +1,13 @@
using System.Linq.Expressions;
using BuildingBlocks.Core.Model;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace BuildingBlocks.EFCore;
@ -25,11 +28,13 @@ public static class Extensions
return services;
}
public static IApplicationBuilder UseMigration<TContext>(this IApplicationBuilder app)
public static IApplicationBuilder UseMigration<TContext>(this IApplicationBuilder app, IWebHostEnvironment env)
where TContext : DbContext, IDbContext
{
MigrateDatabaseAsync<TContext>(app.ApplicationServices).GetAwaiter().GetResult();
SeedDataAsync(app.ApplicationServices).GetAwaiter().GetResult();
if (!env.IsEnvironment("test"))
SeedDataAsync(app.ApplicationServices).GetAwaiter().GetResult();
return app;
}

View File

@ -1,4 +1,5 @@
using BuildingBlocks.Core.Event;
using System.Linq.Expressions;
using BuildingBlocks.Core.Event;
namespace BuildingBlocks.MessageProcessor;
@ -24,6 +25,10 @@ public interface IPersistMessageProcessor
CancellationToken cancellationToken = default)
where TCommand : class, IInternalCommand;
Task<IReadOnlyList<PersistMessage>> GetByFilterAsync(
Expression<Func<PersistMessage, bool>> predicate,
CancellationToken cancellationToken = default);
Task<PersistMessage> ExistMessageAsync(
Guid messageId,
CancellationToken cancellationToken = default);

View File

@ -1,4 +1,5 @@
using System.Text.Json;
using System.Linq.Expressions;
using System.Text.Json;
using Ardalis.GuardClauses;
using BuildingBlocks.Core.Event;
using BuildingBlocks.EFCore;
@ -50,6 +51,12 @@ public class PersistMessageProcessor : IPersistMessageProcessor
cancellationToken);
}
public async Task<IReadOnlyList<PersistMessage>> GetByFilterAsync(Expression<Func<PersistMessage, bool>> predicate, CancellationToken cancellationToken = default)
{
var b = (await _dbContext.PersistMessages.Where(predicate).ToListAsync(cancellationToken)).AsReadOnly();
return b;
}
public Task<PersistMessage> ExistMessageAsync(Guid messageId, CancellationToken cancellationToken = default)
{
return _dbContext.PersistMessages.FirstOrDefaultAsync(x =>

View File

@ -0,0 +1,404 @@
using Ardalis.GuardClauses;
using BuildingBlocks.Core.Model;
using BuildingBlocks.EFCore;
using BuildingBlocks.MassTransit;
using BuildingBlocks.MessageProcessor;
using BuildingBlocks.Mongo;
using BuildingBlocks.Utils;
using BuildingBlocks.Web;
using Grpc.Net.Client;
using MassTransit;
using MassTransit.Testing;
using MediatR;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Mongo2Go;
using NSubstitute;
using Respawn;
using Serilog;
using Xunit;
using Xunit.Abstractions;
namespace BuildingBlocks.TestBase;
public class IntegrationTestFixture<TEntryPoint> : IAsyncLifetime
where TEntryPoint : class
{
private readonly WebApplicationFactory<TEntryPoint> _factory;
private int Timeout => 180;
public Action<IServiceCollection> TestRegistrationServices { set; get; }
public HttpClient HttpClient => _factory.CreateClient();
public ITestHarness TestHarness => CreateHarness();
public GrpcChannel Channel => CreateChannel();
public IServiceProvider ServiceProvider => _factory.Services;
public IConfiguration Configuration => _factory.Services.GetRequiredService<IConfiguration>();
public IntegrationTestFixture()
{
_factory = new WebApplicationFactory<TEntryPoint>()
.WithWebHostBuilder(builder =>
{
builder.UseEnvironment("test");
builder.ConfigureServices(services =>
{
TestRegistrationServices?.Invoke(services);
services.ReplaceSingleton(AddHttpContextAccessorMock);
services.AddMassTransitTestHarness(x =>
{
x.UsingRabbitMq((context, cfg) =>
{
var rabbitMqOptions = services.GetOptions<RabbitMqOptions>("RabbitMq");
var host = rabbitMqOptions.HostName;
cfg.Host(host, h =>
{
h.Username(rabbitMqOptions.UserName);
h.Password(rabbitMqOptions.Password);
});
cfg.ConfigureEndpoints(context);
});
});
});
});
}
public Task InitializeAsync()
{
return Task.CompletedTask;
}
public async Task DisposeAsync()
{
await _factory.DisposeAsync();
}
public virtual void RegisterServices(Action<IServiceCollection> services)
{
TestRegistrationServices = services;
}
// ref: https://github.com/trbenning/serilog-sinks-xunit
public ILogger CreateLogger(ITestOutputHelper output)
{
if (output != null)
return new LoggerConfiguration()
.WriteTo.TestOutput(output)
.CreateLogger();
return null;
}
public async Task ExecuteScopeAsync(Func<IServiceProvider, Task> action)
{
using var scope = ServiceProvider.CreateScope();
await action(scope.ServiceProvider);
}
public async Task<T> ExecuteScopeAsync<T>(Func<IServiceProvider, Task<T>> action)
{
using var scope = ServiceProvider.CreateScope();
var result = await action(scope.ServiceProvider);
return result;
}
public Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request)
{
return ExecuteScopeAsync(sp =>
{
var mediator = sp.GetRequiredService<IMediator>();
return mediator.Send(request);
});
}
public Task SendAsync(IRequest request)
{
return ExecuteScopeAsync(sp =>
{
var mediator = sp.GetRequiredService<IMediator>();
return mediator.Send(request);
});
}
// Ref: https://tech.energyhelpline.com/in-memory-testing-with-masstransit/
public async ValueTask WaitUntilConditionMet(Func<Task<bool>> conditionToMet, int? timeoutSecond = null)
{
var time = timeoutSecond ?? Timeout;
var startTime = DateTime.Now;
var timeoutExpired = false;
var meet = await conditionToMet.Invoke();
while (!meet)
{
if (timeoutExpired) throw new TimeoutException("Condition not met for the test.");
await Task.Delay(100);
meet = await conditionToMet.Invoke();
timeoutExpired = DateTime.Now - startTime > TimeSpan.FromSeconds(time);
}
}
public async ValueTask ShouldProcessedPersistInternalCommand<TInternalCommand>()
where TInternalCommand : class, IInternalCommand
{
await WaitUntilConditionMet(async () =>
{
return await ExecuteScopeAsync(async sp =>
{
var persistMessageProcessor = sp.GetService<IPersistMessageProcessor>();
Guard.Against.Null(persistMessageProcessor, nameof(persistMessageProcessor));
var filter = await persistMessageProcessor.GetByFilterAsync(x =>
x.DeliveryType == MessageDeliveryType.Internal &&
TypeProvider.GetTypeName(typeof(TInternalCommand)) == x.DataType);
var res = filter.Any(x => x.MessageStatus == MessageStatus.Processed);
return res;
});
});
}
private ITestHarness CreateHarness()
{
var harness = ServiceProvider.GetTestHarness();
return harness;
}
private GrpcChannel CreateChannel()
{
return GrpcChannel.ForAddress(HttpClient.BaseAddress!, new GrpcChannelOptions {HttpClient = HttpClient});
}
private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider)
{
var httpContextAccessorMock = Substitute.For<IHttpContextAccessor>();
using var scope = serviceProvider.CreateScope();
httpContextAccessorMock.HttpContext = new DefaultHttpContext {RequestServices = scope.ServiceProvider};
httpContextAccessorMock.HttpContext.Request.Host = new HostString("localhost", 6012);
httpContextAccessorMock.HttpContext.Request.Scheme = "http";
return httpContextAccessorMock;
}
}
public class IntegrationTestFixture<TEntryPoint, TWContext> : IntegrationTestFixture<TEntryPoint>
where TEntryPoint : class
where TWContext : DbContext
{
public Task ExecuteDbContextAsync(Func<TWContext, Task> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<TWContext>()));
}
public Task ExecuteDbContextAsync(Func<TWContext, ValueTask> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<TWContext>()).AsTask());
}
public Task ExecuteDbContextAsync(Func<TWContext, IMediator, Task> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<TWContext>(), sp.GetService<IMediator>()));
}
public Task<T> ExecuteDbContextAsync<T>(Func<TWContext, Task<T>> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<TWContext>()));
}
public Task<T> ExecuteDbContextAsync<T>(Func<TWContext, ValueTask<T>> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<TWContext>()).AsTask());
}
public Task<T> ExecuteDbContextAsync<T>(Func<TWContext, IMediator, Task<T>> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<TWContext>(), sp.GetService<IMediator>()));
}
public Task InsertAsync<T>(params T[] entities) where T : class
{
return ExecuteDbContextAsync(db =>
{
foreach (var entity in entities) db.Set<T>().Add(entity);
return db.SaveChangesAsync();
});
}
public async Task InsertAsync<TEntity>(TEntity entity) where TEntity : class
{
await ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity, TEntity2>(TEntity entity, TEntity2 entity2)
where TEntity : class
where TEntity2 : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity, TEntity2, TEntity3>(TEntity entity, TEntity2 entity2, TEntity3 entity3)
where TEntity : class
where TEntity2 : class
where TEntity3 : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
db.Set<TEntity3>().Add(entity3);
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity, TEntity2, TEntity3, TEntity4>(TEntity entity, TEntity2 entity2, TEntity3 entity3,
TEntity4 entity4)
where TEntity : class
where TEntity2 : class
where TEntity3 : class
where TEntity4 : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
db.Set<TEntity3>().Add(entity3);
db.Set<TEntity4>().Add(entity4);
return db.SaveChangesAsync();
});
}
public Task<T> FindAsync<T>(long id)
where T : class, IEntity
{
return ExecuteDbContextAsync(db => db.Set<T>().FindAsync(id).AsTask());
}
}
public class IntegrationTestFixture<TEntryPoint, TWContext, TRContext> : IntegrationTestFixture<TEntryPoint, TWContext>
where TEntryPoint : class
where TWContext : DbContext
where TRContext : MongoDbContext
{
public Task ExecuteReadContextAsync(Func<TRContext, Task> action)
{
return ExecuteScopeAsync(sp => action(sp.GetRequiredService<TRContext>()));
}
public Task<T> ExecuteReadContextAsync<T>(Func<TRContext, Task<T>> action)
{
return ExecuteScopeAsync(sp => action(sp.GetRequiredService<TRContext>()));
}
}
public class IntegrationTestFixtureCore<TEntryPoint> : IAsyncLifetime
where TEntryPoint : class
{
private Checkpoint _checkpoint;
private MongoDbRunner _mongoRunner;
public IntegrationTestFixtureCore(IntegrationTestFixture<TEntryPoint> integrationTestFixture)
{
Fixture = integrationTestFixture;
integrationTestFixture.RegisterServices(services => RegisterTestsServices(services));
}
public IntegrationTestFixture<TEntryPoint> Fixture { get; }
public async Task InitializeAsync()
{
_checkpoint = new Checkpoint {TablesToIgnore = new[] {"__EFMigrationsHistory"}};
_mongoRunner = MongoDbRunner.Start();
var mongoOptions = Fixture.ServiceProvider.GetRequiredService<IOptions<MongoOptions>>();
if (mongoOptions.Value.ConnectionString != null)
mongoOptions.Value.ConnectionString = _mongoRunner.ConnectionString;
await SeedDataAsync();
}
public async Task DisposeAsync()
{
await _checkpoint.Reset(Fixture.Configuration?.GetConnectionString("DefaultConnection"));
_mongoRunner.Dispose();
}
protected virtual void RegisterTestsServices(IServiceCollection services)
{
}
private async Task SeedDataAsync()
{
using var scope = Fixture.ServiceProvider.CreateScope();
var seeders = scope.ServiceProvider.GetServices<IDataSeeder>();
foreach (var seeder in seeders) await seeder.SeedAllAsync();
}
}
public abstract class IntegrationTestBase<TEntryPoint> : IntegrationTestFixtureCore<TEntryPoint>,
IClassFixture<IntegrationTestFixture<TEntryPoint>>
where TEntryPoint : class
{
protected IntegrationTestBase(
IntegrationTestFixture<TEntryPoint> integrationTestFixture) : base(integrationTestFixture)
{
Fixture = integrationTestFixture;
}
public new IntegrationTestFixture<TEntryPoint> Fixture { get; }
}
public abstract class IntegrationTestBase<TEntryPoint, TWContext> : IntegrationTestFixtureCore<TEntryPoint>,
IClassFixture<IntegrationTestFixture<TEntryPoint, TWContext>>
where TEntryPoint : class
where TWContext : DbContext
{
protected IntegrationTestBase(
IntegrationTestFixture<TEntryPoint, TWContext> integrationTestFixture) : base(integrationTestFixture)
{
Fixture = integrationTestFixture;
}
public new IntegrationTestFixture<TEntryPoint, TWContext> Fixture { get; }
}
public abstract class IntegrationTestBase<TEntryPoint, TWContext, TRContext> : IntegrationTestFixtureCore<TEntryPoint>,
IClassFixture<IntegrationTestFixture<TEntryPoint, TWContext, TRContext>>
where TEntryPoint : class
where TWContext : DbContext
where TRContext : MongoDbContext
{
protected IntegrationTestBase(
IntegrationTestFixture<TEntryPoint, TWContext, TRContext> integrationTestFixture) : base(integrationTestFixture)
{
Fixture = integrationTestFixture;
}
public new IntegrationTestFixture<TEntryPoint, TWContext, TRContext> Fixture { get; }
}

View File

@ -69,7 +69,7 @@ if (app.Environment.IsDevelopment())
}
app.UseSerilogRequestLogging();
app.UseMigration<BookingDbContext>();
app.UseMigration<BookingDbContext>(env);
app.UseCorrelationId();
app.UseRouting();
app.UseHttpMetrics();

View File

@ -5,6 +5,6 @@ namespace Booking.Booking.Features.CreateBooking;
public record CreateBookingCommand(long PassengerId, long FlightId, string Description) : ICommand<ulong>
{
public long Id { get; set; } = SnowFlakIdGenerator.NewId();
public long Id { get; init; } = SnowFlakIdGenerator.NewId();
}

View File

@ -1,29 +1,30 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Booking.Data;
using BuildingBlocks.Contracts.Grpc;
using BuildingBlocks.TestBase;
using FluentAssertions;
using Grpc.Net.Client;
using Integration.Test.Fakes;
using MagicOnion;
using MassTransit.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using NSubstitute;
using Xunit;
namespace Integration.Test.Booking.Features;
public class CreateBookingTests: IClassFixture<IntegrationTestFixture>
{
private readonly GrpcChannel _channel;
private readonly IntegrationTestFixture _fixture;
private readonly ITestHarness _testHarness;
public CreateBookingTests(IntegrationTestFixture fixture)
public class CreateBookingTests : IntegrationTestBase<Program, BookingDbContext>
{
public CreateBookingTests(IntegrationTestFixture<Program, BookingDbContext> integrationTestFixture) : base(
integrationTestFixture)
{
_fixture = fixture;
_testHarness = fixture.TestHarness;
_channel = fixture.Channel;
}
protected override void RegisterTestsServices(IServiceCollection services)
{
MockFlightGrpcServices(services);
MockPassengerGrpcServices(services);
}
// todo: add support test for event-store
@ -34,9 +35,42 @@ public class CreateBookingTests: IClassFixture<IntegrationTestFixture>
var command = new FakeCreateBookingCommand().Generate();
// Act
var response = await _fixture.SendAsync(command);
var response = await Fixture.SendAsync(command);
// Assert
response.Should().BeGreaterOrEqualTo(0);
}
private void MockPassengerGrpcServices(IServiceCollection services)
{
services.Replace(ServiceDescriptor.Singleton(x =>
{
var mock = Substitute.For<IPassengerGrpcService>();
mock.GetById(Arg.Any<long>())
.Returns(new UnaryResult<PassengerResponseDto>(new FakePassengerResponseDto().Generate()));
return mock;
}));
}
private void MockFlightGrpcServices(IServiceCollection services)
{
services.Replace(ServiceDescriptor.Singleton(x =>
{
var mock = Substitute.For<IFlightGrpcService>();
mock.GetById(Arg.Any<long>())
.Returns(new UnaryResult<FlightResponseDto>(Task.FromResult(new FakeFlightResponseDto().Generate())));
mock.GetAvailableSeats(Arg.Any<long>())
.Returns(
new UnaryResult<IEnumerable<SeatResponseDto>>(Task.FromResult(FakeSeatsResponseDto.Generate())));
mock.ReserveSeat(new FakeReserveSeatRequestDto().Generate())
.Returns(new UnaryResult<SeatResponseDto>(Task.FromResult(FakeSeatsResponseDto.Generate().First())));
return mock;
}));
}
}

View File

@ -1,314 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Booking.Data;
using BuildingBlocks.Contracts.Grpc;
using BuildingBlocks.Core.Model;
using BuildingBlocks.MassTransit;
using BuildingBlocks.Mongo;
using BuildingBlocks.Web;
using Grpc.Net.Client;
using Integration.Test.Fakes;
using MagicOnion;
using MassTransit;
using MassTransit.Testing;
using MediatR;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Mongo2Go;
using NSubstitute;
using Respawn;
using Serilog;
using Xunit;
using Xunit.Abstractions;
namespace Integration.Test;
public class IntegrationTestFixture : IAsyncLifetime
{
private Checkpoint _checkpoint;
private IConfiguration _configuration;
private WebApplicationFactory<Program> _factory;
private MongoDbRunner _mongoRunner;
private IServiceProvider _serviceProvider;
private Action<IServiceCollection>? _testRegistrationServices;
public ITestHarness TestHarness { get; private set; }
public HttpClient HttpClient { get; private set; }
public GrpcChannel Channel { get; private set; }
public Task InitializeAsync()
{
_factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.UseEnvironment("test");
builder.ConfigureServices(services =>
{
_testRegistrationServices?.Invoke(services);
});
});
RegisterServices(services =>
{
MockFlightGrpcServices(services);
MockPassengerGrpcServices(services);
services.ReplaceSingleton(AddHttpContextAccessorMock);
services.AddMassTransitTestHarness(x =>
{
x.UsingRabbitMq((context, cfg) =>
{
var rabbitMqOptions = services.GetOptions<RabbitMqOptions>("RabbitMq");
var host = rabbitMqOptions.HostName;
cfg.Host(host, h =>
{
h.Username(rabbitMqOptions.UserName);
h.Password(rabbitMqOptions.Password);
});
cfg.ConfigureEndpoints(context);
});
});
});
_serviceProvider = _factory.Services;
_configuration = _factory.Services.GetRequiredService<IConfiguration>();
HttpClient = _factory.CreateClient();
Channel = CreateChannel();
TestHarness = CreateHarness();
_checkpoint = new Checkpoint {TablesToIgnore = new[] {"__EFMigrationsHistory"}};
_mongoRunner = MongoDbRunner.Start();
var mongoOptions = _factory.Services.GetRequiredService<IOptions<MongoOptions>>();
if (mongoOptions.Value.ConnectionString != null)
mongoOptions.Value.ConnectionString = _mongoRunner.ConnectionString;
return Task.CompletedTask;
}
public async Task DisposeAsync()
{
await _checkpoint.Reset(_configuration?.GetConnectionString("DefaultConnection"));
_mongoRunner.Dispose();
await _factory.DisposeAsync();
}
public void RegisterServices(Action<IServiceCollection> services)
{
_testRegistrationServices = services;
}
// ref: https://github.com/trbenning/serilog-sinks-xunit
public ILogger CreateLogger(ITestOutputHelper output)
{
if (output != null)
{
return new LoggerConfiguration()
.WriteTo.TestOutput(output)
.CreateLogger();
}
return null;
}
public async Task ExecuteScopeAsync(Func<IServiceProvider, Task> action)
{
using var scope = _serviceProvider.CreateScope();
await action(scope.ServiceProvider);
}
public async Task<T> ExecuteScopeAsync<T>(Func<IServiceProvider, Task<T>> action)
{
using var scope = _serviceProvider.CreateScope();
var result = await action(scope.ServiceProvider);
return result;
}
public Task ExecuteDbContextAsync(Func<BookingDbContext, Task> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<BookingDbContext>()));
}
public Task ExecuteDbContextAsync(Func<BookingDbContext, ValueTask> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<BookingDbContext>()).AsTask());
}
public Task ExecuteDbContextAsync(Func<BookingDbContext, IMediator, Task> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<BookingDbContext>(), sp.GetService<IMediator>()));
}
public Task<T> ExecuteDbContextAsync<T>(Func<BookingDbContext, Task<T>> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<BookingDbContext>()));
}
public Task<T> ExecuteDbContextAsync<T>(Func<BookingDbContext, ValueTask<T>> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<BookingDbContext>()).AsTask());
}
public Task<T> ExecuteDbContextAsync<T>(Func<BookingDbContext, IMediator, Task<T>> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<BookingDbContext>(), sp.GetService<IMediator>()));
}
public Task InsertAsync<T>(params T[] entities) where T : class
{
return ExecuteDbContextAsync(db =>
{
foreach (var entity in entities) db.Set<T>().Add(entity);
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity>(TEntity entity) where TEntity : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity, TEntity2>(TEntity entity, TEntity2 entity2)
where TEntity : class
where TEntity2 : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity, TEntity2, TEntity3>(TEntity entity, TEntity2 entity2, TEntity3 entity3)
where TEntity : class
where TEntity2 : class
where TEntity3 : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
db.Set<TEntity3>().Add(entity3);
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity, TEntity2, TEntity3, TEntity4>(TEntity entity, TEntity2 entity2, TEntity3 entity3,
TEntity4 entity4)
where TEntity : class
where TEntity2 : class
where TEntity3 : class
where TEntity4 : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
db.Set<TEntity3>().Add(entity3);
db.Set<TEntity4>().Add(entity4);
return db.SaveChangesAsync();
});
}
public Task<T> FindAsync<T>(long id)
where T : class, IEntity
{
return ExecuteDbContextAsync(db => db.Set<T>().FindAsync(id).AsTask());
}
public Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request)
{
return ExecuteScopeAsync(sp =>
{
var mediator = sp.GetRequiredService<IMediator>();
return mediator.Send(request);
});
}
public Task SendAsync(IRequest request)
{
return ExecuteScopeAsync(sp =>
{
var mediator = sp.GetRequiredService<IMediator>();
return mediator.Send(request);
});
}
private ITestHarness CreateHarness()
{
var harness = _serviceProvider.GetTestHarness();
return harness;
}
private GrpcChannel CreateChannel()
{
return GrpcChannel.ForAddress(HttpClient.BaseAddress!, new GrpcChannelOptions {HttpClient = HttpClient});
}
private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider)
{
var httpContextAccessorMock = Substitute.For<IHttpContextAccessor>();
using var scope = serviceProvider.CreateScope();
httpContextAccessorMock.HttpContext = new DefaultHttpContext {RequestServices = scope.ServiceProvider};
httpContextAccessorMock.HttpContext.Request.Host = new HostString("localhost", 6012);
httpContextAccessorMock.HttpContext.Request.Scheme = "http";
return httpContextAccessorMock;
}
private void MockPassengerGrpcServices(IServiceCollection services)
{
services.Replace(ServiceDescriptor.Singleton(x =>
{
var mock = Substitute.For<IPassengerGrpcService>();
mock.GetById(Arg.Any<long>())
.Returns(new UnaryResult<PassengerResponseDto>(new FakePassengerResponseDto().Generate()));
return mock;
}));
}
private void MockFlightGrpcServices(IServiceCollection services)
{
services.Replace(ServiceDescriptor.Singleton(x =>
{
var mock = Substitute.For<IFlightGrpcService>();
mock.GetById(Arg.Any<long>())
.Returns(new UnaryResult<FlightResponseDto>(Task.FromResult(new FakeFlightResponseDto().Generate())));
mock.GetAvailableSeats(Arg.Any<long>())
.Returns(
new UnaryResult<IEnumerable<SeatResponseDto>>(Task.FromResult(FakeSeatsResponseDto.Generate())));
mock.ReserveSeat(new FakeReserveSeatRequestDto().Generate())
.Returns(new UnaryResult<SeatResponseDto>(Task.FromResult(FakeSeatsResponseDto.Generate().First())));
return mock;
}));
}
}

View File

@ -81,7 +81,7 @@ app.UseSerilogRequestLogging();
app.UseCorrelationId();
app.UseRouting();
app.UseHttpMetrics();
app.UseMigration<FlightDbContext>();
app.UseMigration<FlightDbContext>(env);
app.UseProblemDetails();
app.UseHttpsRedirection();
app.UseCustomHealthCheck();

View File

@ -1,5 +1,6 @@
using BuildingBlocks.IdsGenerator;
using Flight.Aircrafts.Features.CreateAircraft.Reads;
using Flight.Aircrafts.Models;
using Flight.Aircrafts.Models.Reads;
using Mapster;
@ -12,5 +13,9 @@ public class AircraftMappings : IRegister
config.NewConfig<CreateAircraftMongoCommand, AircraftReadModel>()
.Map(d => d.Id, s => SnowFlakIdGenerator.NewId())
.Map(d => d.AircraftId, s => s.Id);
config.NewConfig<Aircraft, AircraftReadModel>()
.Map(d => d.Id, s => SnowFlakIdGenerator.NewId())
.Map(d => d.AircraftId, s => s.Id);
}
}

View File

@ -7,5 +7,5 @@ namespace Flight.Aircrafts.Features.CreateAircraft;
public record CreateAircraftCommand(string Name, string Model, int ManufacturingYear) : ICommand<AircraftResponseDto>, IInternalCommand
{
public long Id { get; set; } = SnowFlakIdGenerator.NewId();
public long Id { get; init; } = SnowFlakIdGenerator.NewId();
}

View File

@ -1,5 +1,6 @@
using BuildingBlocks.IdsGenerator;
using Flight.Airports.Features.CreateAirport.Reads;
using Flight.Airports.Models;
using Flight.Airports.Models.Reads;
using Mapster;
@ -12,5 +13,9 @@ public class AirportMappings : IRegister
config.NewConfig<CreateAirportMongoCommand, AirportReadModel>()
.Map(d => d.Id, s => SnowFlakIdGenerator.NewId())
.Map(d => d.AirportId, s => s.Id);
config.NewConfig<Airport, AirportReadModel>()
.Map(d => d.Id, s => SnowFlakIdGenerator.NewId())
.Map(d => d.AirportId, s => s.Id);
}
}

View File

@ -7,5 +7,5 @@ namespace Flight.Airports.Features.CreateAirport;
public record CreateAirportCommand(string Name, string Address, string Code) : ICommand<AirportResponseDto>, IInternalCommand
{
public long Id { get; set; } = SnowFlakIdGenerator.NewId();
public long Id { get; init; } = SnowFlakIdGenerator.NewId();
}

View File

@ -23,6 +23,7 @@ public sealed class FlightDbContext : AppDbContextBase
protected override void OnModelCreating(ModelBuilder builder)
{
builder.FilterSoftDeletedProperties();
builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
base.OnModelCreating(builder);
}

View File

@ -1,11 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BuildingBlocks.EFCore;
using Flight.Aircrafts.Models;
using Flight.Aircrafts.Models.Reads;
using Flight.Airports.Models;
using Flight.Airports.Models.Reads;
using Flight.Flights.Models;
using Flight.Flights.Models.Reads;
using Flight.Seats.Models;
using Flight.Seats.Models.Reads;
using MapsterMapper;
using Microsoft.EntityFrameworkCore;
namespace Flight.Data.Seed;
@ -13,10 +19,16 @@ namespace Flight.Data.Seed;
public class FlightDataSeeder : IDataSeeder
{
private readonly FlightDbContext _flightDbContext;
private readonly FlightReadDbContext _flightReadDbContext;
private readonly IMapper _mapper;
public FlightDataSeeder(FlightDbContext flightDbContext)
public FlightDataSeeder(FlightDbContext flightDbContext,
FlightReadDbContext flightReadDbContext,
IMapper mapper)
{
_flightDbContext = flightDbContext;
_flightReadDbContext = flightReadDbContext;
_mapper = mapper;
}
public async Task SeedAllAsync()
@ -39,6 +51,7 @@ public class FlightDataSeeder : IDataSeeder
await _flightDbContext.Airports.AddRangeAsync(airports);
await _flightDbContext.SaveChangesAsync();
await _flightReadDbContext.Airport.InsertManyAsync(_mapper.Map<List<AirportReadModel>>(airports));
}
}
@ -55,6 +68,7 @@ public class FlightDataSeeder : IDataSeeder
await _flightDbContext.Aircraft.AddRangeAsync(aircrafts);
await _flightDbContext.SaveChangesAsync();
await _flightReadDbContext.Aircraft.InsertManyAsync(_mapper.Map<List<AircraftReadModel>>(aircrafts));
}
}
@ -75,6 +89,7 @@ public class FlightDataSeeder : IDataSeeder
await _flightDbContext.Seats.AddRangeAsync(seats);
await _flightDbContext.SaveChangesAsync();
await _flightReadDbContext.Seat.InsertManyAsync(_mapper.Map<List<SeatReadModel>>(seats));
}
}
@ -92,6 +107,7 @@ public class FlightDataSeeder : IDataSeeder
};
await _flightDbContext.Flights.AddRangeAsync(flights);
await _flightDbContext.SaveChangesAsync();
await _flightReadDbContext.Flight.InsertManyAsync(_mapper.Map<List<FlightReadModel>>(flights));
}
}
}

View File

@ -6,6 +6,7 @@ public record FlightResponseDto
{
public long Id { get; init; }
public string FlightNumber { get; init; }
public long FlightId { get; set; }
public long AircraftId { get; init; }
public long DepartureAirportId { get; init; }
public DateTime DepartureDate { get; init; }

View File

@ -10,5 +10,5 @@ public record CreateFlightCommand(string FlightNumber, long AircraftId, long Dep
DateTime DepartureDate, DateTime ArriveDate, long ArriveAirportId,
decimal DurationMinutes, DateTime FlightDate, FlightStatus Status, decimal Price) : ICommand<FlightResponseDto>, IInternalCommand
{
public long Id { get; set; } = SnowFlakIdGenerator.NewId();
public long Id { get; init; } = SnowFlakIdGenerator.NewId();
}

View File

@ -15,16 +15,13 @@ namespace Flight.Flights.Features.CreateFlight;
public class CreateFlightCommandHandler : ICommandHandler<CreateFlightCommand, FlightResponseDto>
{
private readonly FlightDbContext _flightDbContext;
private readonly IPersistMessageProcessor _persistMessageProcessor;
private readonly IMapper _mapper;
public CreateFlightCommandHandler(IMapper mapper,
FlightDbContext flightDbContext,
IPersistMessageProcessor persistMessageProcessor)
FlightDbContext flightDbContext)
{
_mapper = mapper;
_flightDbContext = flightDbContext;
_persistMessageProcessor = persistMessageProcessor;
}
public async Task<FlightResponseDto> Handle(CreateFlightCommand command, CancellationToken cancellationToken)

View File

@ -13,10 +13,14 @@ public class FlightMappings : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.NewConfig<Models.Flight, FlightResponseDto>();
config.NewConfig<Models.Flight, FlightResponseDto>()
.Map(d => d.FlightId, s => s.Id);
config.NewConfig<CreateFlightMongoCommand, FlightReadModel>()
.Map(d => d.Id, s => SnowFlakIdGenerator.NewId())
.Map(d => d.FlightId, s => s.Id);
config.NewConfig<Models.Flight, FlightReadModel>()
.Map(d => d.Id, s => SnowFlakIdGenerator.NewId())
.Map(d => d.FlightId, s => s.Id);
config.NewConfig<UpdateFlightMongoCommand, FlightReadModel>()
.Map(d => d.FlightId, s => s.Id);
config.NewConfig<DeleteFlightMongoCommand, FlightReadModel>()

View File

@ -27,7 +27,7 @@ public class GetFlightByIdQueryHandler : IQueryHandler<GetFlightByIdQuery, Fligh
Guard.Against.Null(query, nameof(query));
var flight =
await _flightReadDbContext.Flight.AsQueryable().SingleOrDefaultAsync(x => x.Id == query.Id, cancellationToken);
await _flightReadDbContext.Flight.AsQueryable().SingleOrDefaultAsync(x => x.FlightId == query.Id, cancellationToken);
if (flight is null)
throw new FlightNotFountException();

View File

@ -40,14 +40,12 @@ public class UpdateFlightMongoCommandHandler : ICommandHandler<UpdateFlightMongo
await _flightReadDbContext.Flight.UpdateOneAsync(
x => x.FlightId == flightReadModel.FlightId,
Builders<FlightReadModel>.Update
.Set(x => x.Id, flightReadModel.Id)
.Set(x => x.Price, flightReadModel.Price)
.Set(x => x.ArriveDate, flightReadModel.ArriveDate)
.Set(x => x.AircraftId, flightReadModel.AircraftId)
.Set(x => x.DurationMinutes, flightReadModel.DurationMinutes)
.Set(x => x.DepartureDate, flightReadModel.DepartureDate)
.Set(x => x.FlightDate, flightReadModel.FlightDate)
.Set(x => x.FlightId, flightReadModel.FlightId)
.Set(x => x.FlightNumber, flightReadModel.FlightNumber)
.Set(x => x.IsDeleted, flightReadModel.IsDeleted)
.Set(x => x.Status, flightReadModel.Status)

View File

@ -6,17 +6,17 @@ namespace Flight.Flights.Models.Reads;
public class FlightReadModel
{
public long Id { get; init; }
public long Id { get; set; }
public long FlightId { get; set; }
public string FlightNumber { get; init; }
public long AircraftId { get; init; }
public DateTime DepartureDate { get; init; }
public long DepartureAirportId { get; init; }
public DateTime ArriveDate { get; init; }
public long ArriveAirportId { get; init; }
public decimal DurationMinutes { get; init; }
public DateTime FlightDate { get; init; }
public FlightStatus Status { get; init; }
public decimal Price { get; init; }
public string FlightNumber { get; set; }
public long AircraftId { get; set; }
public DateTime DepartureDate { get; set; }
public long DepartureAirportId { get; set; }
public DateTime ArriveDate { get; set; }
public long ArriveAirportId { get; set; }
public decimal DurationMinutes { get; set; }
public DateTime FlightDate { get; set; }
public FlightStatus Status { get; set; }
public decimal Price { get; set; }
public bool IsDeleted { get; set; }
}

View File

@ -7,5 +7,5 @@ namespace Flight.Seats.Features.CreateSeat;
public record CreateSeatCommand(string SeatNumber, SeatType Type, SeatClass Class, long FlightId) : ICommand<SeatResponseDto>, IInternalCommand
{
public long Id { get; set; } = SnowFlakIdGenerator.NewId();
public long Id { get; init; } = SnowFlakIdGenerator.NewId();
}

View File

@ -29,7 +29,7 @@ public class GetAvailableSeatsQueryHandler : IRequestHandler<GetAvailableSeatsQu
Guard.Against.Null(query, nameof(query));
var seats = (await _flightReadDbContext.Seat.AsQueryable().ToListAsync(cancellationToken))
.Where(x => !x.IsDeleted);
.Where(x => x.FlightId == query.FlightId);
if (!seats.Any())
throw new AllSeatsFullException();

View File

@ -16,6 +16,9 @@ public class SeatMappings : IRegister
config.NewConfig<CreateSeatMongoCommand, SeatReadModel>()
.Map(d => d.Id, s => SnowFlakIdGenerator.NewId())
.Map(d => d.SeatId, s => s.Id);
config.NewConfig<Seat, SeatReadModel>()
.Map(d => d.Id, s => SnowFlakIdGenerator.NewId())
.Map(d => d.SeatId, s => s.Id);
config.NewConfig<ReserveSeatMongoCommand, SeatReadModel>()
.Map(d => d.SeatId, s => s.Id);
}

View File

@ -1,20 +1,25 @@
using System.Threading.Tasks;
using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.TestBase;
using Flight.Aircrafts.Features.CreateAircraft.Reads;
using Flight.Airports.Features.CreateAirport.Reads;
using Flight.Data;
using FluentAssertions;
using Grpc.Net.Client;
using Integration.Test.Fakes;
using MassTransit;
using MassTransit.Testing;
using Xunit;
namespace Integration.Test.Aircraft.Features;
public class CreateAircraftTests : IClassFixture<IntegrationTestFixture>
public class CreateAircraftTests : IntegrationTestBase<Program, FlightDbContext, FlightReadDbContext>
{
private readonly IntegrationTestFixture _fixture;
private readonly ITestHarness _testHarness;
public CreateAircraftTests(IntegrationTestFixture fixture)
public CreateAircraftTests(IntegrationTestFixture<Program, FlightDbContext, FlightReadDbContext> integrationTestFixture) : base(integrationTestFixture)
{
_fixture = fixture;
_testHarness = fixture.TestHarness;
_testHarness = Fixture.TestHarness;
}
[Fact]
@ -24,12 +29,14 @@ public class CreateAircraftTests : IClassFixture<IntegrationTestFixture>
var command = new FakeCreateAircraftCommand().Generate();
// Act
var response = await _fixture.SendAsync(command);
var response = await Fixture.SendAsync(command);
// Assert
response?.Should().NotBeNull();
response?.Name.Should().Be(command.Name);
(await _testHarness.Published.Any<Fault<AircraftCreated>>()).Should().BeFalse();
(await _testHarness.Published.Any<AircraftCreated>()).Should().BeTrue();
await Fixture.ShouldProcessedPersistInternalCommand<CreateAircraftMongoCommand>();
}
}

View File

@ -1,5 +1,9 @@
using System.Threading.Tasks;
using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.TestBase;
using Flight.Aircrafts.Features.CreateAircraft.Reads;
using Flight.Airports.Features.CreateAirport.Reads;
using Flight.Data;
using FluentAssertions;
using Integration.Test.Fakes;
using MassTransit;
@ -7,15 +11,16 @@ using MassTransit.Testing;
using Xunit;
namespace Integration.Test.Airport.Features;
public class CreateAirportTests : IClassFixture<IntegrationTestFixture>
public class CreateAirportTests : IntegrationTestBase<Program, FlightDbContext, FlightReadDbContext>
{
private readonly IntegrationTestFixture _fixture;
private readonly ITestHarness _testHarness;
public CreateAirportTests(IntegrationTestFixture fixture)
public CreateAirportTests(
IntegrationTestFixture<Program, FlightDbContext, FlightReadDbContext> integrationTestFixture) : base(
integrationTestFixture)
{
_fixture = fixture;
_testHarness = fixture.TestHarness;
_testHarness = Fixture.TestHarness;
}
[Fact]
@ -25,12 +30,14 @@ public class CreateAirportTests : IClassFixture<IntegrationTestFixture>
var command = new FakeCreateAirportCommand().Generate();
// Act
var response = await _fixture.SendAsync(command);
var response = await Fixture.SendAsync(command);
// Assert
response?.Should().NotBeNull();
response?.Name.Should().Be(command.Name);
(await _testHarness.Published.Any<Fault<AirportCreated>>()).Should().BeFalse();
(await _testHarness.Published.Any<AirportCreated>()).Should().BeTrue();
await Fixture.ShouldProcessedPersistInternalCommand<CreateAirportMongoCommand>();
}
}

View File

@ -5,13 +5,15 @@ namespace Integration.Test.Fakes;
public class FakeUpdateFlightCommand : AutoFaker<UpdateFlightCommand>
{
public FakeUpdateFlightCommand(long id)
public FakeUpdateFlightCommand(global::Flight.Flights.Models.Flight flight)
{
RuleFor(r => r.Id, _ => id);
RuleFor(r => r.DepartureAirportId, _ => 2);
RuleFor(r => r.ArriveAirportId, _ => 1);
RuleFor(r => r.AircraftId, _ => 2);
RuleFor(r => r.FlightNumber, _ => "12BB");
RuleFor(r => r.Id, _ => flight.Id);
RuleFor(r => r.DepartureAirportId, _ => flight.DepartureAirportId);
RuleFor(r => r.ArriveAirportId, _ => flight.ArriveAirportId);
RuleFor(r => r.AircraftId, _ => flight.AircraftId);
RuleFor(r => r.FlightNumber, _ => "12UU");
RuleFor(r => r.Price, _ => 800);
RuleFor(r => r.Status, _ => flight.Status);
RuleFor(r => r.ArriveDate, _ => flight.ArriveDate);
}
}

View File

@ -1,5 +1,8 @@
using System.Threading.Tasks;
using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.TestBase;
using Flight.Data;
using Flight.Flights.Features.CreateFlight.Reads;
using FluentAssertions;
using Integration.Test.Fakes;
using MassTransit;
@ -8,15 +11,15 @@ using Xunit;
namespace Integration.Test.Flight.Features;
public class CreateFlightTests : IClassFixture<IntegrationTestFixture>
public class CreateFlightTests : IntegrationTestBase<Program, FlightDbContext, FlightReadDbContext>
{
private readonly IntegrationTestFixture _fixture;
private readonly ITestHarness _testHarness;
public CreateFlightTests(IntegrationTestFixture fixture)
public CreateFlightTests(
IntegrationTestFixture<Program, FlightDbContext, FlightReadDbContext> integrationTestFixture) : base(
integrationTestFixture)
{
_fixture = fixture;
_testHarness = fixture.TestHarness;
_testHarness = Fixture.TestHarness;
}
[Fact]
@ -26,7 +29,7 @@ public class CreateFlightTests : IClassFixture<IntegrationTestFixture>
var command = new FakeCreateFlightCommand().Generate();
// Act
var response = await _fixture.SendAsync(command);
var response = await Fixture.SendAsync(command);
// Assert
response.Should().NotBeNull();
@ -34,5 +37,7 @@ public class CreateFlightTests : IClassFixture<IntegrationTestFixture>
(await _testHarness.Published.Any<Fault<FlightCreated>>()).Should().BeFalse();
(await _testHarness.Published.Any<FlightCreated>()).Should().BeTrue();
await Fixture.ShouldProcessedPersistInternalCommand<CreateFlightMongoCommand>();
}
}

View File

@ -1,9 +1,11 @@
using System.Linq;
using System.Threading.Tasks;
using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.TestBase;
using Flight.Data;
using Flight.Flights.Features.DeleteFlight;
using Flight.Flights.Features.DeleteFlight.Reads;
using FluentAssertions;
using Integration.Test.Fakes;
using MassTransit;
using MassTransit.Testing;
using Microsoft.EntityFrameworkCore;
@ -11,33 +13,27 @@ using Xunit;
namespace Integration.Test.Flight.Features;
public class DeleteFlightTests : IClassFixture<IntegrationTestFixture>
public class DeleteFlightTests : IntegrationTestBase<Program, FlightDbContext, FlightReadDbContext>
{
private readonly IntegrationTestFixture _fixture;
private readonly ITestHarness _testHarness;
public DeleteFlightTests(IntegrationTestFixture fixture)
public DeleteFlightTests(
IntegrationTestFixture<Program, FlightDbContext, FlightReadDbContext> integrationTestFixture) : base(
integrationTestFixture)
{
_fixture = fixture;
_testHarness = fixture.TestHarness;
_testHarness = Fixture.TestHarness;
}
[Fact]
public async Task should_delete_flight_from_db()
{
// Arrange
var createFlightCommand = new FakeCreateFlightCommand().Generate();
var flightEntity = global::Flight.Flights.Models.Flight.Create(
createFlightCommand.Id, createFlightCommand.FlightNumber, createFlightCommand.AircraftId, createFlightCommand.DepartureAirportId,
createFlightCommand.DepartureDate, createFlightCommand.ArriveDate, createFlightCommand.ArriveAirportId, createFlightCommand.DurationMinutes,
createFlightCommand.FlightDate, createFlightCommand.Status, createFlightCommand.Price);
await _fixture.InsertAsync(flightEntity);
var flightEntity = await Fixture.FindAsync<global::Flight.Flights.Models.Flight>(1);
var command = new DeleteFlightCommand(flightEntity.Id);
// Act
await _fixture.SendAsync(command);
var deletedFlight = (await _fixture.ExecuteDbContextAsync(db => db.Flights
await Fixture.SendAsync(command);
var deletedFlight = (await Fixture.ExecuteDbContextAsync(db => db.Flights
.Where(x => x.Id == command.Id)
.IgnoreQueryFilters()
.ToListAsync())
@ -47,6 +43,6 @@ public class DeleteFlightTests : IClassFixture<IntegrationTestFixture>
deletedFlight?.IsDeleted.Should().BeTrue();
(await _testHarness.Published.Any<Fault<FlightDeleted>>()).Should().BeFalse();
(await _testHarness.Published.Any<FlightDeleted>()).Should().BeTrue();
await Fixture.ShouldProcessedPersistInternalCommand<DeleteFlightMongoCommand>();
}
}

View File

@ -1,42 +1,37 @@
using System.Linq;
using System.Threading.Tasks;
using BuildingBlocks.Contracts.Grpc;
using BuildingBlocks.TestBase;
using Flight.Data;
using Flight.Flights.Features.CreateFlight.Reads;
using Flight.Flights.Features.GetAvailableFlights;
using FluentAssertions;
using Grpc.Net.Client;
using Integration.Test.Fakes;
using MagicOnion.Client;
using Xunit;
namespace Integration.Test.Flight.Features;
public class GetAvailableFlightsTests : IClassFixture<IntegrationTestFixture>
public class GetAvailableFlightsTests : IntegrationTestBase<Program, FlightDbContext, FlightReadDbContext>
{
private readonly IntegrationTestFixture _fixture;
private readonly GrpcChannel _channel;
public GetAvailableFlightsTests(IntegrationTestFixture fixture)
public GetAvailableFlightsTests(
IntegrationTestFixture<Program, FlightDbContext, FlightReadDbContext> integrationTestFixture)
: base(integrationTestFixture)
{
_fixture = fixture;
_channel = fixture.Channel;
}
[Fact]
public async Task should_return_available_flights()
{
// Arrange
var flightCommand1 = new FakeCreateFlightCommand().Generate();
var flightCommand2 = new FakeCreateFlightCommand().Generate();
var flightCommand = new FakeCreateFlightCommand().Generate();
var flightEntity1 = FakeFlightCreated.Generate(flightCommand1);
var flightEntity2 = FakeFlightCreated.Generate(flightCommand2);
await Fixture.SendAsync(flightCommand);
await _fixture.InsertAsync(flightEntity1, flightEntity2);
await Fixture.ShouldProcessedPersistInternalCommand<CreateFlightMongoCommand>();
var query = new GetAvailableFlightsQuery();
// Act
var response = (await _fixture.SendAsync(query))?.ToList();
var response = (await Fixture.SendAsync(query))?.ToList();
// Assert
response?.Should().NotBeNull();

View File

@ -1,5 +1,8 @@
using System.Threading.Tasks;
using BuildingBlocks.Contracts.Grpc;
using BuildingBlocks.TestBase;
using Flight.Data;
using Flight.Flights.Features.CreateFlight.Reads;
using Flight.Flights.Features.GetFlightById;
using FluentAssertions;
using Grpc.Net.Client;
@ -9,50 +12,50 @@ using Xunit;
namespace Integration.Test.Flight.Features;
public class GetFlightByIdTests : IClassFixture<IntegrationTestFixture>
public class GetFlightByIdTests : IntegrationTestBase<Program, FlightDbContext, FlightReadDbContext>
{
private readonly IntegrationTestFixture _fixture;
private readonly GrpcChannel _channel;
public GetFlightByIdTests(IntegrationTestFixture fixture)
public GetFlightByIdTests(IntegrationTestFixture<Program, FlightDbContext, FlightReadDbContext> integrationTestFixture) : base(integrationTestFixture)
{
_fixture = fixture;
_channel = fixture.Channel;
_channel = Fixture.Channel;
}
[Fact]
public async Task should_retrive_a_flight_by_id_currectly()
{
// Arrange
//Arrange
var command = new FakeCreateFlightCommand().Generate();
var flightEntity = FakeFlightCreated.Generate(command);
await _fixture.InsertAsync(flightEntity);
await Fixture.SendAsync(command);
var query = new GetFlightByIdQuery(flightEntity.Id);
await Fixture.ShouldProcessedPersistInternalCommand<CreateFlightMongoCommand>();
var query = new GetFlightByIdQuery(command.Id);
// Act
var response = await _fixture.SendAsync(query);
var response = await Fixture.SendAsync(query);
// Assert
response.Should().NotBeNull();
response?.Id.Should().Be(flightEntity.Id);
response?.FlightId.Should().Be(command.Id);
}
[Fact]
public async Task should_retrive_a_flight_by_id_from_grpc_service()
{
// Arrange
//Arrange
var command = new FakeCreateFlightCommand().Generate();
var flightEntity = FakeFlightCreated.Generate(command);
await _fixture.InsertAsync(flightEntity);
await Fixture.SendAsync(command);
await Fixture.ShouldProcessedPersistInternalCommand<CreateFlightMongoCommand>();
var flightGrpcClient = MagicOnionClient.Create<IFlightGrpcService>(_channel);
// Act
var response = await flightGrpcClient.GetById(flightEntity.Id);
var response = await flightGrpcClient.GetById(command.Id);
// Assert
response?.Should().NotBeNull();
response?.Id.Should().Be(flightEntity.Id);
response?.FlightId.Should().Be(command.Id);
}
}

View File

@ -1,5 +1,8 @@
using System.Threading.Tasks;
using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.TestBase;
using Flight.Data;
using Flight.Flights.Features.UpdateFlight.Reads;
using FluentAssertions;
using Integration.Test.Fakes;
using MassTransit;
@ -7,29 +10,25 @@ using MassTransit.Testing;
using Xunit;
namespace Integration.Test.Flight.Features;
public class UpdateFlightTests : IClassFixture<IntegrationTestFixture>
public class UpdateFlightTests : IntegrationTestBase<Program, FlightDbContext, FlightReadDbContext>
{
private readonly IntegrationTestFixture _fixture;
private readonly ITestHarness _testHarness;
public UpdateFlightTests(IntegrationTestFixture fixture)
public UpdateFlightTests(IntegrationTestFixture<Program, FlightDbContext, FlightReadDbContext> integrationTestFixture) : base(integrationTestFixture)
{
_fixture = fixture;
_testHarness = fixture.TestHarness;
_testHarness = Fixture.TestHarness;
}
[Fact]
public async Task should_update_flight_to_db_and_publish_message_to_broker()
{
// Arrange
var fakeCreateCommandFlight = new FakeCreateFlightCommand().Generate();
var flightEntity = FakeFlightCreated.Generate(fakeCreateCommandFlight);
await _fixture.InsertAsync(flightEntity);
var command = new FakeUpdateFlightCommand(flightEntity.Id).Generate();
var flightEntity = await Fixture.FindAsync<global::Flight.Flights.Models.Flight>(1);
var command = new FakeUpdateFlightCommand(flightEntity).Generate();
// Act
var response = await _fixture.SendAsync(command);
var response = await Fixture.SendAsync(command);
// Assert
response.Should().NotBeNull();
@ -37,5 +36,6 @@ public class UpdateFlightTests : IClassFixture<IntegrationTestFixture>
response?.Price.Should().NotBe(flightEntity?.Price);
(await _testHarness.Published.Any<Fault<FlightUpdated>>()).Should().BeFalse();
(await _testHarness.Published.Any<FlightUpdated>()).Should().BeTrue();
await Fixture.ShouldProcessedPersistInternalCommand<UpdateFlightMongoCommand>();
}
}

View File

@ -1,273 +0,0 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using BuildingBlocks.Core.Model;
using BuildingBlocks.MassTransit;
using BuildingBlocks.Mongo;
using BuildingBlocks.Web;
using Flight.Data;
using Grpc.Net.Client;
using MassTransit;
using MassTransit.Testing;
using MediatR;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Mongo2Go;
using NSubstitute;
using Respawn;
using Serilog;
using Xunit;
using Xunit.Abstractions;
namespace Integration.Test;
public class IntegrationTestFixture : IAsyncLifetime
{
private Checkpoint _checkpoint;
private IConfiguration _configuration;
private WebApplicationFactory<Program> _factory;
private MongoDbRunner _mongoRunner;
private IServiceProvider _serviceProvider;
private Action<IServiceCollection>? _testRegistrationServices;
public ITestHarness TestHarness { get; private set; }
public HttpClient HttpClient { get; private set; }
public GrpcChannel Channel { get; private set; }
public Task InitializeAsync()
{
_factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.UseEnvironment("test");
builder.ConfigureServices(services =>
{
_testRegistrationServices?.Invoke(services);
});
});
RegisterServices(services =>
{
services.ReplaceSingleton(AddHttpContextAccessorMock);
services.AddMassTransitTestHarness(x =>
{
x.UsingRabbitMq((context, cfg) =>
{
var rabbitMqOptions = services.GetOptions<RabbitMqOptions>("RabbitMq");
var host = rabbitMqOptions.HostName;
cfg.Host(host, h =>
{
h.Username(rabbitMqOptions.UserName);
h.Password(rabbitMqOptions.Password);
});
cfg.ConfigureEndpoints(context);
});
});
});
_serviceProvider = _factory.Services;
_configuration = _factory.Services.GetRequiredService<IConfiguration>();
HttpClient = _factory.CreateClient();
Channel = CreateChannel();
TestHarness = CreateHarness();
_checkpoint = new Checkpoint {TablesToIgnore = new[] {"__EFMigrationsHistory"}};
_mongoRunner = MongoDbRunner.Start();
var mongoOptions = _factory.Services.GetRequiredService<IOptions<MongoOptions>>();
if (mongoOptions.Value.ConnectionString != null)
mongoOptions.Value.ConnectionString = _mongoRunner.ConnectionString;
return Task.CompletedTask;
}
public async Task DisposeAsync()
{
await _checkpoint.Reset(_configuration?.GetConnectionString("DefaultConnection"));
_mongoRunner.Dispose();
await _factory.DisposeAsync();
}
public void RegisterServices(Action<IServiceCollection> services)
{
_testRegistrationServices = services;
}
// ref: https://github.com/trbenning/serilog-sinks-xunit
public ILogger CreateLogger(ITestOutputHelper output)
{
if (output != null)
{
return new LoggerConfiguration()
.WriteTo.TestOutput(output)
.CreateLogger();
}
return null;
}
public async Task ExecuteScopeAsync(Func<IServiceProvider, Task> action)
{
using var scope = _serviceProvider.CreateScope();
await action(scope.ServiceProvider);
}
public async Task<T> ExecuteScopeAsync<T>(Func<IServiceProvider, Task<T>> action)
{
using var scope = _serviceProvider.CreateScope();
var result = await action(scope.ServiceProvider);
return result;
}
public Task ExecuteDbContextAsync(Func<FlightDbContext, Task> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<FlightDbContext>()));
}
public Task ExecuteDbContextAsync(Func<FlightDbContext, ValueTask> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<FlightDbContext>()).AsTask());
}
public Task ExecuteDbContextAsync(Func<FlightDbContext, IMediator, Task> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<FlightDbContext>(), sp.GetService<IMediator>()));
}
public Task<T> ExecuteDbContextAsync<T>(Func<FlightDbContext, Task<T>> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<FlightDbContext>()));
}
public Task<T> ExecuteDbContextAsync<T>(Func<FlightDbContext, ValueTask<T>> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<FlightDbContext>()).AsTask());
}
public Task<T> ExecuteDbContextAsync<T>(Func<FlightDbContext, IMediator, Task<T>> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<FlightDbContext>(), sp.GetService<IMediator>()));
}
public Task InsertAsync<T>(params T[] entities) where T : class
{
return ExecuteDbContextAsync(db =>
{
foreach (var entity in entities) db.Set<T>().Add(entity);
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity>(TEntity entity) where TEntity : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity, TEntity2>(TEntity entity, TEntity2 entity2)
where TEntity : class
where TEntity2 : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity, TEntity2, TEntity3>(TEntity entity, TEntity2 entity2, TEntity3 entity3)
where TEntity : class
where TEntity2 : class
where TEntity3 : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
db.Set<TEntity3>().Add(entity3);
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity, TEntity2, TEntity3, TEntity4>(TEntity entity, TEntity2 entity2, TEntity3 entity3,
TEntity4 entity4)
where TEntity : class
where TEntity2 : class
where TEntity3 : class
where TEntity4 : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
db.Set<TEntity3>().Add(entity3);
db.Set<TEntity4>().Add(entity4);
return db.SaveChangesAsync();
});
}
public Task<T> FindAsync<T>(long id)
where T : class, IEntity
{
return ExecuteDbContextAsync(db => db.Set<T>().FindAsync(id).AsTask());
}
public Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request)
{
return ExecuteScopeAsync(sp =>
{
var mediator = sp.GetRequiredService<IMediator>();
return mediator.Send(request);
});
}
public Task SendAsync(IRequest request)
{
return ExecuteScopeAsync(sp =>
{
var mediator = sp.GetRequiredService<IMediator>();
return mediator.Send(request);
});
}
private ITestHarness CreateHarness()
{
var harness = _serviceProvider.GetTestHarness();
return harness;
}
private GrpcChannel CreateChannel()
{
return GrpcChannel.ForAddress(HttpClient.BaseAddress!, new GrpcChannelOptions {HttpClient = HttpClient});
}
private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider)
{
var httpContextAccessorMock = Substitute.For<IHttpContextAccessor>();
using var scope = serviceProvider.CreateScope();
httpContextAccessorMock.HttpContext = new DefaultHttpContext {RequestServices = scope.ServiceProvider};
httpContextAccessorMock.HttpContext.Request.Host = new HostString("localhost", 6012);
httpContextAccessorMock.HttpContext.Request.Scheme = "http";
return httpContextAccessorMock;
}
}

View File

@ -1,23 +1,26 @@
using System.Linq;
using System.Threading.Tasks;
using BuildingBlocks.Contracts.Grpc;
using BuildingBlocks.TestBase;
using Flight.Data;
using Flight.Flights.Features.CreateFlight.Reads;
using Flight.Seats.Features.CreateSeat.Reads;
using FluentAssertions;
using Grpc.Net.Client;
using Integration.Test.Fakes;
using MagicOnion.Client;
using MassTransit.Testing;
using Xunit;
namespace Integration.Test.Seat.Features;
public class GetAvailableSeatsTests : IClassFixture<IntegrationTestFixture>
public class GetAvailableSeatsTests : IntegrationTestBase<Program, FlightDbContext, FlightReadDbContext>
{
private readonly IntegrationTestFixture _fixture;
private readonly GrpcChannel _channel;
public GetAvailableSeatsTests(IntegrationTestFixture fixture)
public GetAvailableSeatsTests(IntegrationTestFixture<Program, FlightDbContext, FlightReadDbContext> integrationTestFixture) : base(integrationTestFixture)
{
_fixture = fixture;
_channel = fixture.Channel;
_channel = Fixture.Channel;
}
[Fact]
@ -25,24 +28,24 @@ public class GetAvailableSeatsTests : IClassFixture<IntegrationTestFixture>
{
// Arrange
var flightCommand = new FakeCreateFlightCommand().Generate();
var flightEntity = FakeFlightCreated.Generate(flightCommand);
await _fixture.InsertAsync(flightEntity);
await Fixture.SendAsync(flightCommand);
var seatCommand1 = new FakeCreateSeatCommand(flightEntity.Id).Generate();
var seatCommand2 = new FakeCreateSeatCommand(flightEntity.Id).Generate();
var seatEntity1 = FakeSeatCreated.Generate(seatCommand1);
var seatEntity2 = FakeSeatCreated.Generate(seatCommand2);
await Fixture.ShouldProcessedPersistInternalCommand<CreateFlightMongoCommand>();
await _fixture.InsertAsync<global::Flight.Seats.Models.Seat, global::Flight.Seats.Models.Seat>(seatEntity1, seatEntity2);
var seatCommand = new FakeCreateSeatCommand(flightCommand.Id).Generate();
await Fixture.SendAsync(seatCommand);
await Fixture.ShouldProcessedPersistInternalCommand<CreateSeatMongoCommand>();
var flightGrpcClient = MagicOnionClient.Create<IFlightGrpcService>(_channel);
// Act
var response = await flightGrpcClient.GetAvailableSeats(flightEntity.Id);
var response = await flightGrpcClient.GetAvailableSeats(flightCommand.Id);
// Assert
response?.Should().NotBeNull();
response?.Count().Should().BeGreaterOrEqualTo(2);
response?.Count().Should().BeGreaterOrEqualTo(1);
}
}

View File

@ -1,5 +1,9 @@
using System.Threading.Tasks;
using BuildingBlocks.Contracts.Grpc;
using BuildingBlocks.TestBase;
using Flight.Data;
using Flight.Flights.Features.CreateFlight.Reads;
using Flight.Seats.Features.CreateSeat.Reads;
using FluentAssertions;
using Grpc.Net.Client;
using Integration.Test.Fakes;
@ -7,15 +11,16 @@ using MagicOnion.Client;
using Xunit;
namespace Integration.Test.Seat.Features;
public class ReserveSeatTests : IClassFixture<IntegrationTestFixture>
public class ReserveSeatTests : IntegrationTestBase<Program, FlightDbContext, FlightReadDbContext>
{
private readonly IntegrationTestFixture _fixture;
private readonly GrpcChannel _channel;
public ReserveSeatTests(IntegrationTestFixture fixture)
public ReserveSeatTests(
IntegrationTestFixture<Program, FlightDbContext, FlightReadDbContext> integrationTestFixture) : base(
integrationTestFixture)
{
_fixture = fixture;
_channel = fixture.Channel;
_channel = Fixture.Channel;
}
[Fact]
@ -23,23 +28,28 @@ public class ReserveSeatTests : IClassFixture<IntegrationTestFixture>
{
// Arrange
var flightCommand = new FakeCreateFlightCommand().Generate();
var flightEntity = FakeFlightCreated.Generate(flightCommand);
await _fixture.InsertAsync(flightEntity);
await Fixture.SendAsync(flightCommand);
var seatCommand = new FakeCreateSeatCommand(flightEntity.Id).Generate();
var seatEntity = FakeSeatCreated.Generate(seatCommand);
await Fixture.ShouldProcessedPersistInternalCommand<CreateFlightMongoCommand>();
await _fixture.InsertAsync(seatEntity);
var seatCommand = new FakeCreateSeatCommand(flightCommand.Id).Generate();
await Fixture.SendAsync(seatCommand);
await Fixture.ShouldProcessedPersistInternalCommand<CreateSeatMongoCommand>();
var flightGrpcClient = MagicOnionClient.Create<IFlightGrpcService>(_channel);
// Act
var response = await flightGrpcClient.ReserveSeat(new ReserveSeatRequestDto{ FlightId = seatEntity.FlightId, SeatNumber = seatEntity.SeatNumber });
var response = await flightGrpcClient.ReserveSeat(new ReserveSeatRequestDto
{
FlightId = seatCommand.FlightId, SeatNumber = seatCommand.SeatNumber
});
// Assert
response?.Should().NotBeNull();
response?.SeatNumber.Should().Be(seatEntity.SeatNumber);
response?.FlightId.Should().Be(seatEntity.FlightId);
response?.SeatNumber.Should().Be(seatCommand.SeatNumber);
response?.FlightId.Should().Be(seatCommand.FlightId);
}
}

View File

@ -25,7 +25,7 @@ public class CreateFlightCommandHandlerTests
public CreateFlightCommandHandlerTests(UnitTestFixture fixture)
{
_fixture = fixture;
_handler = new CreateFlightCommandHandler(fixture.Mapper, fixture.DbContext, Substitute.For<IPersistMessageProcessor>());
_handler = new CreateFlightCommandHandler(fixture.Mapper, fixture.DbContext);
}
[Fact]

View File

@ -57,7 +57,7 @@ if (app.Environment.IsDevelopment())
}
app.UseSerilogRequestLogging();
app.UseMigration<IdentityContext>();
app.UseMigration<IdentityContext>(env);
app.UseCorrelationId();
app.UseRouting();
app.UseHttpMetrics();

View File

@ -1,6 +1,9 @@
using System.Threading.Tasks;
using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.TestBase;
using FluentAssertions;
using Grpc.Net.Client;
using Identity.Data;
using Integration.Test.Fakes;
using MassTransit;
using MassTransit.Testing;
@ -8,15 +11,13 @@ using Xunit;
namespace Integration.Test.Identity.Features;
public class RegisterNewUserTests : IClassFixture<IntegrationTestFixture>
public class RegisterNewUserTests : IntegrationTestBase<Program, IdentityContext>
{
private readonly IntegrationTestFixture _fixture;
private readonly ITestHarness _testHarness;
public RegisterNewUserTests(IntegrationTestFixture fixture)
public RegisterNewUserTests(IntegrationTestFixture<Program, IdentityContext> integrationTestFixture) : base(integrationTestFixture)
{
_fixture = fixture;
_testHarness = _fixture.TestHarness;
_testHarness = Fixture.TestHarness;
}
[Fact]
@ -26,7 +27,7 @@ public class RegisterNewUserTests : IClassFixture<IntegrationTestFixture>
var command = new FakeRegisterNewUserCommand().Generate();
// Act
var response = await _fixture.SendAsync(command);
var response = await Fixture.SendAsync(command);
// Assert
response?.Should().NotBeNull();

View File

@ -1,271 +0,0 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using BuildingBlocks.Core.Model;
using BuildingBlocks.MassTransit;
using BuildingBlocks.Mongo;
using BuildingBlocks.Web;
using Grpc.Net.Client;
using Identity.Data;
using MassTransit;
using MassTransit.Testing;
using MediatR;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Mongo2Go;
using NSubstitute;
using Respawn;
using Serilog;
using Xunit;
using Xunit.Abstractions;
namespace Integration.Test;
public class IntegrationTestFixture : IAsyncLifetime
{
private Checkpoint _checkpoint;
private IConfiguration _configuration;
private WebApplicationFactory<Program> _factory;
private MongoDbRunner _mongoRunner;
private IServiceProvider _serviceProvider;
private Action<IServiceCollection>? _testRegistrationServices;
public ITestHarness TestHarness { get; private set; }
public HttpClient HttpClient { get; private set; }
public GrpcChannel Channel { get; private set; }
public Task InitializeAsync()
{
_factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.UseEnvironment("test");
builder.ConfigureServices(services =>
{
_testRegistrationServices?.Invoke(services);
});
});
RegisterServices(services =>
{
services.ReplaceSingleton(AddHttpContextAccessorMock);
services.AddMassTransitTestHarness(x =>
{
x.UsingRabbitMq((context, cfg) =>
{
var rabbitMqOptions = services.GetOptions<RabbitMqOptions>("RabbitMq");
var host = rabbitMqOptions.HostName;
cfg.Host(host, h =>
{
h.Username(rabbitMqOptions.UserName);
h.Password(rabbitMqOptions.Password);
});
cfg.ConfigureEndpoints(context);
});
});
});
_serviceProvider = _factory.Services;
_configuration = _factory.Services.GetRequiredService<IConfiguration>();
HttpClient = _factory.CreateClient();
Channel = CreateChannel();
TestHarness = CreateHarness();
_checkpoint = new Checkpoint {TablesToIgnore = new[] {"__EFMigrationsHistory"}};
_mongoRunner = MongoDbRunner.Start();
var mongoOptions = _factory.Services.GetRequiredService<IOptions<MongoOptions>>();
if (mongoOptions.Value.ConnectionString != null)
mongoOptions.Value.ConnectionString = _mongoRunner.ConnectionString;
return Task.CompletedTask;
}
public async Task DisposeAsync()
{
await _checkpoint.Reset(_configuration?.GetConnectionString("DefaultConnection"));
_mongoRunner.Dispose();
await _factory.DisposeAsync();
}
public void RegisterServices(Action<IServiceCollection> services)
{
_testRegistrationServices = services;
}
// ref: https://github.com/trbenning/serilog-sinks-xunit
public ILogger CreateLogger(ITestOutputHelper output)
{
if (output != null)
return new LoggerConfiguration()
.WriteTo.TestOutput(output)
.CreateLogger();
return null;
}
public async Task ExecuteScopeAsync(Func<IServiceProvider, Task> action)
{
using var scope = _serviceProvider.CreateScope();
await action(scope.ServiceProvider);
}
public async Task<T> ExecuteScopeAsync<T>(Func<IServiceProvider, Task<T>> action)
{
using var scope = _serviceProvider.CreateScope();
var result = await action(scope.ServiceProvider);
return result;
}
public Task ExecuteDbContextAsync(Func<IdentityContext, Task> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<IdentityContext>()));
}
public Task ExecuteDbContextAsync(Func<IdentityContext, ValueTask> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<IdentityContext>()).AsTask());
}
public Task ExecuteDbContextAsync(Func<IdentityContext, IMediator, Task> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<IdentityContext>(), sp.GetService<IMediator>()));
}
public Task<T> ExecuteDbContextAsync<T>(Func<IdentityContext, Task<T>> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<IdentityContext>()));
}
public Task<T> ExecuteDbContextAsync<T>(Func<IdentityContext, ValueTask<T>> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<IdentityContext>()).AsTask());
}
public Task<T> ExecuteDbContextAsync<T>(Func<IdentityContext, IMediator, Task<T>> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<IdentityContext>(), sp.GetService<IMediator>()));
}
public Task InsertAsync<T>(params T[] entities) where T : class
{
return ExecuteDbContextAsync(db =>
{
foreach (var entity in entities) db.Set<T>().Add(entity);
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity>(TEntity entity) where TEntity : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity, TEntity2>(TEntity entity, TEntity2 entity2)
where TEntity : class
where TEntity2 : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity, TEntity2, TEntity3>(TEntity entity, TEntity2 entity2, TEntity3 entity3)
where TEntity : class
where TEntity2 : class
where TEntity3 : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
db.Set<TEntity3>().Add(entity3);
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity, TEntity2, TEntity3, TEntity4>(TEntity entity, TEntity2 entity2, TEntity3 entity3,
TEntity4 entity4)
where TEntity : class
where TEntity2 : class
where TEntity3 : class
where TEntity4 : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
db.Set<TEntity3>().Add(entity3);
db.Set<TEntity4>().Add(entity4);
return db.SaveChangesAsync();
});
}
public Task<T> FindAsync<T>(long id)
where T : class, IEntity
{
return ExecuteDbContextAsync(db => db.Set<T>().FindAsync(id).AsTask());
}
public Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request)
{
return ExecuteScopeAsync(sp =>
{
var mediator = sp.GetRequiredService<IMediator>();
return mediator.Send(request);
});
}
public Task SendAsync(IRequest request)
{
return ExecuteScopeAsync(sp =>
{
var mediator = sp.GetRequiredService<IMediator>();
return mediator.Send(request);
});
}
private ITestHarness CreateHarness()
{
var harness = _serviceProvider.GetTestHarness();
return harness;
}
private GrpcChannel CreateChannel()
{
return GrpcChannel.ForAddress(HttpClient.BaseAddress!, new GrpcChannelOptions {HttpClient = HttpClient});
}
private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider)
{
var httpContextAccessorMock = Substitute.For<IHttpContextAccessor>();
using var scope = serviceProvider.CreateScope();
httpContextAccessorMock.HttpContext = new DefaultHttpContext {RequestServices = scope.ServiceProvider};
httpContextAccessorMock.HttpContext.Request.Host = new HostString("localhost", 6012);
httpContextAccessorMock.HttpContext.Request.Scheme = "http";
return httpContextAccessorMock;
}
}

View File

@ -62,7 +62,7 @@ if (app.Environment.IsDevelopment())
}
app.UseSerilogRequestLogging();
app.UseMigration<PassengerDbContext>();
app.UseMigration<PassengerDbContext>(env);
app.UseCorrelationId();
app.UseRouting();
app.UseHttpMetrics();

View File

@ -7,5 +7,5 @@ namespace Passenger.Passengers.Features.CompleteRegisterPassenger;
public record CompleteRegisterPassengerCommand(string PassportNumber, PassengerType PassengerType, int Age) : ICommand<PassengerResponseDto>
{
public long Id { get; set; } = SnowFlakIdGenerator.NewId();
public long Id { get; init; } = SnowFlakIdGenerator.NewId();
}

View File

@ -1,12 +1,11 @@
using AutoBogus;
using Passenger.Passengers.Dtos;
namespace Integration.Test.Fakes;
using BuildingBlocks.Contracts.Grpc;
using BuildingBlocks.IdsGenerator;
public class FakePassengerResponseDto : AutoFaker<PassengerResponseDto>
{
public FakePassengerResponseDto(long id)
public FakePassengerResponseDto()
{
RuleFor(r => r.Id, _ => id);
RuleFor(r => r.Id, _ => SnowFlakIdGenerator.NewId());
}
}

View File

@ -1,273 +0,0 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using BuildingBlocks.Core.Model;
using BuildingBlocks.MassTransit;
using BuildingBlocks.Mongo;
using BuildingBlocks.Web;
using Grpc.Net.Client;
using MassTransit;
using MassTransit.Testing;
using MediatR;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Mongo2Go;
using NSubstitute;
using Passenger.Data;
using Respawn;
using Serilog;
using Xunit;
using Xunit.Abstractions;
namespace Integration.Test;
public class IntegrationTestFixture : IAsyncLifetime
{
private Checkpoint _checkpoint;
private IConfiguration _configuration;
private WebApplicationFactory<Program> _factory;
private MongoDbRunner _mongoRunner;
private IServiceProvider _serviceProvider;
private Action<IServiceCollection>? _testRegistrationServices;
public ITestHarness TestHarness { get; private set; }
public HttpClient HttpClient { get; private set; }
public GrpcChannel Channel { get; private set; }
public Task InitializeAsync()
{
_factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.UseEnvironment("test");
builder.ConfigureServices(services =>
{
_testRegistrationServices?.Invoke(services);
});
});
RegisterServices(services =>
{
services.ReplaceSingleton(AddHttpContextAccessorMock);
services.AddMassTransitTestHarness(x =>
{
x.UsingRabbitMq((context, cfg) =>
{
var rabbitMqOptions = services.GetOptions<RabbitMqOptions>("RabbitMq");
var host = rabbitMqOptions.HostName;
cfg.Host(host, h =>
{
h.Username(rabbitMqOptions.UserName);
h.Password(rabbitMqOptions.Password);
});
cfg.ConfigureEndpoints(context);
});
});
});
_serviceProvider = _factory.Services;
_configuration = _factory.Services.GetRequiredService<IConfiguration>();
HttpClient = _factory.CreateClient();
Channel = CreateChannel();
TestHarness = CreateHarness();
_checkpoint = new Checkpoint {TablesToIgnore = new[] {"__EFMigrationsHistory"}};
_mongoRunner = MongoDbRunner.Start();
var mongoOptions = _factory.Services.GetRequiredService<IOptions<MongoOptions>>();
if (mongoOptions.Value.ConnectionString != null)
mongoOptions.Value.ConnectionString = _mongoRunner.ConnectionString;
return Task.CompletedTask;
}
public async Task DisposeAsync()
{
await _checkpoint.Reset(_configuration?.GetConnectionString("DefaultConnection"));
_mongoRunner.Dispose();
await _factory.DisposeAsync();
}
public void RegisterServices(Action<IServiceCollection> services)
{
_testRegistrationServices = services;
}
// ref: https://github.com/trbenning/serilog-sinks-xunit
public ILogger CreateLogger(ITestOutputHelper output)
{
if (output != null)
{
return new LoggerConfiguration()
.WriteTo.TestOutput(output)
.CreateLogger();
}
return null;
}
public async Task ExecuteScopeAsync(Func<IServiceProvider, Task> action)
{
using var scope = _serviceProvider.CreateScope();
await action(scope.ServiceProvider);
}
public async Task<T> ExecuteScopeAsync<T>(Func<IServiceProvider, Task<T>> action)
{
using var scope = _serviceProvider.CreateScope();
var result = await action(scope.ServiceProvider);
return result;
}
public Task ExecuteDbContextAsync(Func<PassengerDbContext, Task> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<PassengerDbContext>()));
}
public Task ExecuteDbContextAsync(Func<PassengerDbContext, ValueTask> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<PassengerDbContext>()).AsTask());
}
public Task ExecuteDbContextAsync(Func<PassengerDbContext, IMediator, Task> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<PassengerDbContext>(), sp.GetService<IMediator>()));
}
public Task<T> ExecuteDbContextAsync<T>(Func<PassengerDbContext, Task<T>> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<PassengerDbContext>()));
}
public Task<T> ExecuteDbContextAsync<T>(Func<PassengerDbContext, ValueTask<T>> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<PassengerDbContext>()).AsTask());
}
public Task<T> ExecuteDbContextAsync<T>(Func<PassengerDbContext, IMediator, Task<T>> action)
{
return ExecuteScopeAsync(sp => action(sp.GetService<PassengerDbContext>(), sp.GetService<IMediator>()));
}
public Task InsertAsync<T>(params T[] entities) where T : class
{
return ExecuteDbContextAsync(db =>
{
foreach (var entity in entities) db.Set<T>().Add(entity);
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity>(TEntity entity) where TEntity : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity, TEntity2>(TEntity entity, TEntity2 entity2)
where TEntity : class
where TEntity2 : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity, TEntity2, TEntity3>(TEntity entity, TEntity2 entity2, TEntity3 entity3)
where TEntity : class
where TEntity2 : class
where TEntity3 : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
db.Set<TEntity3>().Add(entity3);
return db.SaveChangesAsync();
});
}
public Task InsertAsync<TEntity, TEntity2, TEntity3, TEntity4>(TEntity entity, TEntity2 entity2, TEntity3 entity3,
TEntity4 entity4)
where TEntity : class
where TEntity2 : class
where TEntity3 : class
where TEntity4 : class
{
return ExecuteDbContextAsync(db =>
{
db.Set<TEntity>().Add(entity);
db.Set<TEntity2>().Add(entity2);
db.Set<TEntity3>().Add(entity3);
db.Set<TEntity4>().Add(entity4);
return db.SaveChangesAsync();
});
}
public Task<T> FindAsync<T>(long id)
where T : class, IEntity
{
return ExecuteDbContextAsync(db => db.Set<T>().FindAsync(id).AsTask());
}
public Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request)
{
return ExecuteScopeAsync(sp =>
{
var mediator = sp.GetRequiredService<IMediator>();
return mediator.Send(request);
});
}
public Task SendAsync(IRequest request)
{
return ExecuteScopeAsync(sp =>
{
var mediator = sp.GetRequiredService<IMediator>();
return mediator.Send(request);
});
}
private ITestHarness CreateHarness()
{
var harness = _serviceProvider.GetTestHarness();
return harness;
}
private GrpcChannel CreateChannel()
{
return GrpcChannel.ForAddress(HttpClient.BaseAddress!, new GrpcChannelOptions {HttpClient = HttpClient});
}
private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider)
{
var httpContextAccessorMock = Substitute.For<IHttpContextAccessor>();
using var scope = serviceProvider.CreateScope();
httpContextAccessorMock.HttpContext = new DefaultHttpContext {RequestServices = scope.ServiceProvider};
httpContextAccessorMock.HttpContext.Request.Host = new HostString("localhost", 6012);
httpContextAccessorMock.HttpContext.Request.Scheme = "http";
return httpContextAccessorMock;
}
}

View File

@ -1,21 +1,21 @@
using System.Threading.Tasks;
using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.TestBase;
using FluentAssertions;
using Integration.Test.Fakes;
using MassTransit.Testing;
using Passenger.Data;
using Xunit;
namespace Integration.Test.Passenger.Features;
public class CompleteRegisterPassengerTests : IClassFixture<IntegrationTestFixture>
public class CompleteRegisterPassengerTests : IntegrationTestBase<Program, PassengerDbContext>
{
private readonly IntegrationTestFixture _fixture;
private readonly ITestHarness _testHarness;
public CompleteRegisterPassengerTests(IntegrationTestFixture fixture)
public CompleteRegisterPassengerTests(IntegrationTestFixture<Program, PassengerDbContext> integrationTestFixture) : base(integrationTestFixture)
{
_fixture = fixture;
_testHarness = _fixture.TestHarness;
_testHarness = Fixture.TestHarness;
}
[Fact]
@ -25,12 +25,12 @@ public class CompleteRegisterPassengerTests : IClassFixture<IntegrationTestFixtu
var userCreated = new FakeUserCreated().Generate();
await _testHarness.Bus.Publish(userCreated);
await _testHarness.Consumed.Any<UserCreated>();
await _fixture.InsertAsync(FakePassengerCreated.Generate(userCreated));
await Fixture.InsertAsync(FakePassengerCreated.Generate(userCreated));
var command = new FakeCompleteRegisterPassengerCommand(userCreated.PassportNumber).Generate();
// Act
var response = await _fixture.SendAsync(command);
var response = await Fixture.SendAsync(command);
// Assert
response.Should().NotBeNull();

View File

@ -1,29 +1,39 @@
using System.Threading.Tasks;
using BuildingBlocks.Contracts.EventBus.Messages;
using BuildingBlocks.Contracts.Grpc;
using BuildingBlocks.TestBase;
using FluentAssertions;
using Grpc.Net.Client;
using Integration.Test.Fakes;
using MagicOnion;
using MagicOnion.Client;
using MassTransit.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using NSubstitute;
using Passenger.Data;
using Passenger.Passengers.Features.GetPassengerById;
using Xunit;
namespace Integration.Test.Passenger.Features;
public class GetPassengerByIdTests : IClassFixture<IntegrationTestFixture>
public class GetPassengerByIdTests : IntegrationTestBase<Program, PassengerDbContext>
{
private readonly IntegrationTestFixture _fixture;
private readonly ITestHarness _testHarness;
private readonly GrpcChannel _channel;
public GetPassengerByIdTests(IntegrationTestFixture fixture)
public GetPassengerByIdTests(IntegrationTestFixture<Program, PassengerDbContext> integrationTestFixture) : base(integrationTestFixture)
{
_fixture = fixture;
_testHarness = _fixture.TestHarness;
_channel = _fixture.Channel;
_channel = Fixture.Channel;
_testHarness = Fixture.TestHarness;
}
protected override void RegisterTestsServices(IServiceCollection services)
{
MockPassengerGrpcServices(services);
}
[Fact]
public async Task should_retrive_a_passenger_by_id_currectly()
{
@ -32,12 +42,12 @@ public class GetPassengerByIdTests : IClassFixture<IntegrationTestFixture>
await _testHarness.Bus.Publish(userCreated);
await _testHarness.Consumed.Any<UserCreated>();
var passengerEntity = FakePassengerCreated.Generate(userCreated);
await _fixture.InsertAsync(passengerEntity);
await Fixture.InsertAsync(passengerEntity);
var query = new GetPassengerQueryById(passengerEntity.Id);
// Act
var response = await _fixture.SendAsync(query);
var response = await Fixture.SendAsync(query);
// Assert
response.Should().NotBeNull();
@ -52,7 +62,7 @@ public class GetPassengerByIdTests : IClassFixture<IntegrationTestFixture>
await _testHarness.Bus.Publish(userCreated);
await _testHarness.Consumed.Any<UserCreated>();
var passengerEntity = FakePassengerCreated.Generate(userCreated);
await _fixture.InsertAsync(passengerEntity);
await Fixture.InsertAsync(passengerEntity);
var passengerGrpcClient = MagicOnionClient.Create<IPassengerGrpcService>(_channel);
@ -63,4 +73,16 @@ public class GetPassengerByIdTests : IClassFixture<IntegrationTestFixture>
response?.Should().NotBeNull();
response?.Id.Should().Be(passengerEntity.Id);
}
private void MockPassengerGrpcServices(IServiceCollection services)
{
services.Replace(ServiceDescriptor.Singleton(x =>
{
var mock = Substitute.For<IPassengerGrpcService>();
mock.GetById(Arg.Any<long>())
.Returns(new UnaryResult<PassengerResponseDto>(new FakePassengerResponseDto().Generate()));
return mock;
}));
}
}