diff --git a/src/Services/Booking/src/Booking/Protos/foo.proto b/src/Services/Booking/src/Booking/Protos/foo.proto new file mode 100644 index 0000000..8b225d8 --- /dev/null +++ b/src/Services/Booking/src/Booking/Protos/foo.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; +option csharp_namespace = "GrpcSamples"; + +service FooService { + rpc GetFoo (FooRequest) returns (FooResponse); + + rpc GetFoos(FooServerStreamingRequest) returns (stream FooResponse); + + rpc SendFoos(stream FooRequest) returns (FooResponse); + + rpc SendAndGetFoos(stream FooRequest) returns (stream FooResponse); +} + +message FooRequest { + string message = 1; +} + +message FooServerStreamingRequest { + string message = 1; + int32 messageCount = 2; +} + +message FooResponse { + string message = 1; +} diff --git a/src/Services/Booking/tests/IntegrationTest/CustomWebApplicationFactory.cs b/src/Services/Booking/tests/IntegrationTest/CustomWebApplicationFactory.cs new file mode 100644 index 0000000..87db371 --- /dev/null +++ b/src/Services/Booking/tests/IntegrationTest/CustomWebApplicationFactory.cs @@ -0,0 +1,80 @@ +using System; +using BuildingBlocks.MassTransit; +using BuildingBlocks.Web; +using MassTransit; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using NSubstitute; +using Respawn; + +namespace Integration.Test; + +public class CustomWebApplicationFactory : WebApplicationFactory +{ + public Checkpoint Checkpoint { get; set; } + public IConfiguration Configuration => Services.GetRequiredService(); + public Action? TestRegistrationServices { get; set; } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + //https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests#set-the-environment + //https://stackoverflow.com/questions/43927955/should-getenvironmentvariable-work-in-xunit-test/43951218 + + //we could read env from our test launch setting or we can set it directly here + builder.UseEnvironment("test"); + + //The test app's builder.ConfigureTestServices callback is executed after the app's Startup.ConfigureServices code is executed. + builder.ConfigureTestServices(services => + { + services.RemoveAll(typeof(IHostedService)); + services.ReplaceSingleton(AddHttpContextAccessorMock); + TestRegistrationServices?.Invoke(services); + services.AddMassTransitTestHarness(x => + { + x.UsingRabbitMq((context, cfg) => + { + var rabbitMqOptions = services.GetOptions("RabbitMq"); + var host = rabbitMqOptions.HostName; + + cfg.Host(host, h => + { + h.Username(rabbitMqOptions.UserName); + h.Password(rabbitMqOptions.Password); + }); + cfg.ConfigureEndpoints(context); + }); + }); + + Checkpoint = new Checkpoint {TablesToIgnore = new[] {"__EFMigrationsHistory"}}; + + TestRegistrationServices?.Invoke(services); + }); + + builder.UseDefaultServiceProvider((env, c) => + { + // Handling Captive Dependency Problem + // https://ankitvijay.net/2020/03/17/net-core-and-di-beware-of-captive-dependency/ + // https://blog.ploeh.dk/2014/06/02/captive-dependency/ + if (env.HostingEnvironment.IsEnvironment("test") || env.HostingEnvironment.IsDevelopment()) + c.ValidateScopes = true; + }); + } + + private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider) + { + var httpContextAccessorMock = Substitute.For(); + 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; + } +} \ No newline at end of file diff --git a/src/Services/Booking/tests/IntegrationTest/Integration.Test.csproj b/src/Services/Booking/tests/IntegrationTest/Integration.Test.csproj new file mode 100644 index 0000000..6f7dfd6 --- /dev/null +++ b/src/Services/Booking/tests/IntegrationTest/Integration.Test.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + false + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Services/Booking/tests/IntegrationTest/IntegrationTestFixture.cs b/src/Services/Booking/tests/IntegrationTest/IntegrationTestFixture.cs new file mode 100644 index 0000000..b0e5f05 --- /dev/null +++ b/src/Services/Booking/tests/IntegrationTest/IntegrationTestFixture.cs @@ -0,0 +1,240 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Booking.Data; +using BuildingBlocks.Domain.Model; +using BuildingBlocks.EFCore; +using Grpc.Net.Client; +using MassTransit.Testing; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using Serilog; +using Xunit; +using Xunit.Abstractions; + +namespace Integration.Test; + +[CollectionDefinition(nameof(IntegrationTestFixture))] +public class SliceFixtureCollection : ICollectionFixture { } + +public class IntegrationTestFixture : IAsyncLifetime +{ + private readonly CustomWebApplicationFactory _factory; + + public IntegrationTestFixture() + { + // Ref: https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-6.0#basic-tests-with-the-default-webapplicationfactory + _factory = new CustomWebApplicationFactory(); + } + + public IServiceProvider ServiceProvider => _factory.Services; + public IConfiguration Configuration => _factory.Configuration; + public HttpClient HttpClient => _factory.CreateClient(); + public ITestHarness TestHarness => CreateHarness(); + public GrpcChannel Channel => CreateChannel(); + + // 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 void RegisterTestServices(Action services) => _factory.TestRegistrationServices = services; + + public virtual Task InitializeAsync() + { + return Task.CompletedTask; + } + + public virtual async Task DisposeAsync() + { + if (!string.IsNullOrEmpty(Configuration?.GetConnectionString("DefaultConnection"))) + await _factory.Checkpoint.Reset(Configuration?.GetConnectionString("DefaultConnection")); + + await _factory.DisposeAsync(); + } + + public async Task ExecuteScopeAsync(Func action) + { + using var scope = ServiceProvider.CreateScope(); + await action(scope.ServiceProvider); + } + + public async Task ExecuteScopeAsync(Func> action) + { + using var scope = ServiceProvider.CreateScope(); + + var result = await action(scope.ServiceProvider); + + return result; + } + + public Task ExecuteDbContextAsync(Func action) + { + return ExecuteScopeAsync(sp => action(sp.GetService())); + } + + public Task ExecuteDbContextAsync(Func action) + { + return ExecuteScopeAsync(sp => action(sp.GetService()).AsTask()); + } + + public Task ExecuteDbContextAsync(Func action) + { + return ExecuteScopeAsync(sp => action(sp.GetService(), sp.GetService())); + } + + public Task ExecuteDbContextAsync(Func> action) + { + return ExecuteScopeAsync(sp => action(sp.GetService())); + } + + public Task ExecuteDbContextAsync(Func> action) + { + return ExecuteScopeAsync(sp => action(sp.GetService()).AsTask()); + } + + public Task ExecuteDbContextAsync(Func> action) + { + return ExecuteScopeAsync(sp => action(sp.GetService(), sp.GetService())); + } + + public Task InsertAsync(params T[] entities) where T : class + { + return ExecuteDbContextAsync(db => + { + foreach (var entity in entities) db.Set().Add(entity); + + return db.SaveChangesAsync(); + }); + } + + public Task InsertAsync(TEntity entity) where TEntity : class + { + return ExecuteDbContextAsync(db => + { + db.Set().Add(entity); + + return db.SaveChangesAsync(); + }); + } + + public Task InsertAsync(TEntity entity, TEntity2 entity2) + where TEntity : class + where TEntity2 : class + { + return ExecuteDbContextAsync(db => + { + db.Set().Add(entity); + db.Set().Add(entity2); + + return db.SaveChangesAsync(); + }); + } + + public Task InsertAsync(TEntity entity, TEntity2 entity2, TEntity3 entity3) + where TEntity : class + where TEntity2 : class + where TEntity3 : class + { + return ExecuteDbContextAsync(db => + { + db.Set().Add(entity); + db.Set().Add(entity2); + db.Set().Add(entity3); + + return db.SaveChangesAsync(); + }); + } + + public Task InsertAsync(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().Add(entity); + db.Set().Add(entity2); + db.Set().Add(entity3); + db.Set().Add(entity4); + + return db.SaveChangesAsync(); + }); + } + + public Task FindAsync(long id) + where T : class, IEntity + { + return ExecuteDbContextAsync(db => db.Set().FindAsync(id).AsTask()); + } + + public Task SendAsync(IRequest request) + { + return ExecuteScopeAsync(sp => + { + var mediator = sp.GetRequiredService(); + + return mediator.Send(request); + }); + } + + public Task SendAsync(IRequest request) + { + return ExecuteScopeAsync(sp => + { + var mediator = sp.GetRequiredService(); + + return mediator.Send(request); + }); + } + + private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider) + { + var httpContextAccessorMock = Substitute.For(); + using var scope = serviceProvider.CreateScope(); + httpContextAccessorMock.HttpContext = new DefaultHttpContext {RequestServices = scope.ServiceProvider}; + + httpContextAccessorMock.HttpContext.Request.Host = new HostString("localhost", 5000); + httpContextAccessorMock.HttpContext.Request.Scheme = "http"; + + return httpContextAccessorMock; + } + + private ITestHarness CreateHarness() + { + var harness = ServiceProvider.GetTestHarness(); + harness.Start().GetAwaiter().GetResult(); + return harness; + } + + private GrpcChannel CreateChannel() + { + return GrpcChannel.ForAddress(HttpClient.BaseAddress!, new GrpcChannelOptions {HttpClient = HttpClient}); + } + + private async Task EnsureDatabaseAsync() + { + using var scope = ServiceProvider.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + var seeders = scope.ServiceProvider.GetServices(); + + await context.Database.MigrateAsync(); + + foreach (var seeder in seeders) await seeder.SeedAllAsync(); + } +} diff --git a/src/Services/Flight/tests/IntegrationTest/CustomWebApplicationFactory.cs b/src/Services/Flight/tests/IntegrationTest/CustomWebApplicationFactory.cs new file mode 100644 index 0000000..87db371 --- /dev/null +++ b/src/Services/Flight/tests/IntegrationTest/CustomWebApplicationFactory.cs @@ -0,0 +1,80 @@ +using System; +using BuildingBlocks.MassTransit; +using BuildingBlocks.Web; +using MassTransit; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using NSubstitute; +using Respawn; + +namespace Integration.Test; + +public class CustomWebApplicationFactory : WebApplicationFactory +{ + public Checkpoint Checkpoint { get; set; } + public IConfiguration Configuration => Services.GetRequiredService(); + public Action? TestRegistrationServices { get; set; } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + //https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests#set-the-environment + //https://stackoverflow.com/questions/43927955/should-getenvironmentvariable-work-in-xunit-test/43951218 + + //we could read env from our test launch setting or we can set it directly here + builder.UseEnvironment("test"); + + //The test app's builder.ConfigureTestServices callback is executed after the app's Startup.ConfigureServices code is executed. + builder.ConfigureTestServices(services => + { + services.RemoveAll(typeof(IHostedService)); + services.ReplaceSingleton(AddHttpContextAccessorMock); + TestRegistrationServices?.Invoke(services); + services.AddMassTransitTestHarness(x => + { + x.UsingRabbitMq((context, cfg) => + { + var rabbitMqOptions = services.GetOptions("RabbitMq"); + var host = rabbitMqOptions.HostName; + + cfg.Host(host, h => + { + h.Username(rabbitMqOptions.UserName); + h.Password(rabbitMqOptions.Password); + }); + cfg.ConfigureEndpoints(context); + }); + }); + + Checkpoint = new Checkpoint {TablesToIgnore = new[] {"__EFMigrationsHistory"}}; + + TestRegistrationServices?.Invoke(services); + }); + + builder.UseDefaultServiceProvider((env, c) => + { + // Handling Captive Dependency Problem + // https://ankitvijay.net/2020/03/17/net-core-and-di-beware-of-captive-dependency/ + // https://blog.ploeh.dk/2014/06/02/captive-dependency/ + if (env.HostingEnvironment.IsEnvironment("test") || env.HostingEnvironment.IsDevelopment()) + c.ValidateScopes = true; + }); + } + + private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider) + { + var httpContextAccessorMock = Substitute.For(); + 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; + } +} \ No newline at end of file diff --git a/src/Services/Flight/tests/IntegrationTest/IntegrationTestFixture.cs b/src/Services/Flight/tests/IntegrationTest/IntegrationTestFixture.cs new file mode 100644 index 0000000..0bdc9d5 --- /dev/null +++ b/src/Services/Flight/tests/IntegrationTest/IntegrationTestFixture.cs @@ -0,0 +1,240 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using BuildingBlocks.Domain.Model; +using BuildingBlocks.EFCore; +using Flight.Data; +using Grpc.Net.Client; +using MassTransit.Testing; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using Serilog; +using Xunit; +using Xunit.Abstractions; + +namespace Integration.Test; + +[CollectionDefinition(nameof(IntegrationTestFixture))] +public class SliceFixtureCollection : ICollectionFixture { } + +public class IntegrationTestFixture : IAsyncLifetime +{ + private readonly CustomWebApplicationFactory _factory; + + public IntegrationTestFixture() + { + // Ref: https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-6.0#basic-tests-with-the-default-webapplicationfactory + _factory = new CustomWebApplicationFactory(); + } + + public IServiceProvider ServiceProvider => _factory.Services; + public IConfiguration Configuration => _factory.Configuration; + public HttpClient HttpClient => _factory.CreateClient(); + public ITestHarness TestHarness => CreateHarness(); + public GrpcChannel Channel => CreateChannel(); + + // 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 void RegisterTestServices(Action services) => _factory.TestRegistrationServices = services; + + public virtual Task InitializeAsync() + { + return Task.CompletedTask; + } + + public virtual async Task DisposeAsync() + { + if (!string.IsNullOrEmpty(Configuration?.GetConnectionString("DefaultConnection"))) + await _factory.Checkpoint.Reset(Configuration?.GetConnectionString("DefaultConnection")); + + await _factory.DisposeAsync(); + } + + public async Task ExecuteScopeAsync(Func action) + { + using var scope = ServiceProvider.CreateScope(); + await action(scope.ServiceProvider); + } + + public async Task ExecuteScopeAsync(Func> action) + { + using var scope = ServiceProvider.CreateScope(); + + var result = await action(scope.ServiceProvider); + + return result; + } + + public Task ExecuteDbContextAsync(Func action) + { + return ExecuteScopeAsync(sp => action(sp.GetService())); + } + + public Task ExecuteDbContextAsync(Func action) + { + return ExecuteScopeAsync(sp => action(sp.GetService()).AsTask()); + } + + public Task ExecuteDbContextAsync(Func action) + { + return ExecuteScopeAsync(sp => action(sp.GetService(), sp.GetService())); + } + + public Task ExecuteDbContextAsync(Func> action) + { + return ExecuteScopeAsync(sp => action(sp.GetService())); + } + + public Task ExecuteDbContextAsync(Func> action) + { + return ExecuteScopeAsync(sp => action(sp.GetService()).AsTask()); + } + + public Task ExecuteDbContextAsync(Func> action) + { + return ExecuteScopeAsync(sp => action(sp.GetService(), sp.GetService())); + } + + public Task InsertAsync(params T[] entities) where T : class + { + return ExecuteDbContextAsync(db => + { + foreach (var entity in entities) db.Set().Add(entity); + + return db.SaveChangesAsync(); + }); + } + + public Task InsertAsync(TEntity entity) where TEntity : class + { + return ExecuteDbContextAsync(db => + { + db.Set().Add(entity); + + return db.SaveChangesAsync(); + }); + } + + public Task InsertAsync(TEntity entity, TEntity2 entity2) + where TEntity : class + where TEntity2 : class + { + return ExecuteDbContextAsync(db => + { + db.Set().Add(entity); + db.Set().Add(entity2); + + return db.SaveChangesAsync(); + }); + } + + public Task InsertAsync(TEntity entity, TEntity2 entity2, TEntity3 entity3) + where TEntity : class + where TEntity2 : class + where TEntity3 : class + { + return ExecuteDbContextAsync(db => + { + db.Set().Add(entity); + db.Set().Add(entity2); + db.Set().Add(entity3); + + return db.SaveChangesAsync(); + }); + } + + public Task InsertAsync(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().Add(entity); + db.Set().Add(entity2); + db.Set().Add(entity3); + db.Set().Add(entity4); + + return db.SaveChangesAsync(); + }); + } + + public Task FindAsync(long id) + where T : class, IEntity + { + return ExecuteDbContextAsync(db => db.Set().FindAsync(id).AsTask()); + } + + public Task SendAsync(IRequest request) + { + return ExecuteScopeAsync(sp => + { + var mediator = sp.GetRequiredService(); + + return mediator.Send(request); + }); + } + + public Task SendAsync(IRequest request) + { + return ExecuteScopeAsync(sp => + { + var mediator = sp.GetRequiredService(); + + return mediator.Send(request); + }); + } + + private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider) + { + var httpContextAccessorMock = Substitute.For(); + using var scope = serviceProvider.CreateScope(); + httpContextAccessorMock.HttpContext = new DefaultHttpContext {RequestServices = scope.ServiceProvider}; + + httpContextAccessorMock.HttpContext.Request.Host = new HostString("localhost", 5000); + httpContextAccessorMock.HttpContext.Request.Scheme = "http"; + + return httpContextAccessorMock; + } + + private ITestHarness CreateHarness() + { + var harness = ServiceProvider.GetTestHarness(); + harness.Start().GetAwaiter().GetResult(); + return harness; + } + + private GrpcChannel CreateChannel() + { + return GrpcChannel.ForAddress(HttpClient.BaseAddress!, new GrpcChannelOptions {HttpClient = HttpClient}); + } + + private async Task EnsureDatabaseAsync() + { + using var scope = ServiceProvider.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + var seeders = scope.ServiceProvider.GetServices(); + + await context.Database.MigrateAsync(); + + foreach (var seeder in seeders) await seeder.SeedAllAsync(); + } +} diff --git a/src/Services/Identity/tests/IntegrationTest/CustomWebApplicationFactory.cs b/src/Services/Identity/tests/IntegrationTest/CustomWebApplicationFactory.cs new file mode 100644 index 0000000..87db371 --- /dev/null +++ b/src/Services/Identity/tests/IntegrationTest/CustomWebApplicationFactory.cs @@ -0,0 +1,80 @@ +using System; +using BuildingBlocks.MassTransit; +using BuildingBlocks.Web; +using MassTransit; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using NSubstitute; +using Respawn; + +namespace Integration.Test; + +public class CustomWebApplicationFactory : WebApplicationFactory +{ + public Checkpoint Checkpoint { get; set; } + public IConfiguration Configuration => Services.GetRequiredService(); + public Action? TestRegistrationServices { get; set; } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + //https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests#set-the-environment + //https://stackoverflow.com/questions/43927955/should-getenvironmentvariable-work-in-xunit-test/43951218 + + //we could read env from our test launch setting or we can set it directly here + builder.UseEnvironment("test"); + + //The test app's builder.ConfigureTestServices callback is executed after the app's Startup.ConfigureServices code is executed. + builder.ConfigureTestServices(services => + { + services.RemoveAll(typeof(IHostedService)); + services.ReplaceSingleton(AddHttpContextAccessorMock); + TestRegistrationServices?.Invoke(services); + services.AddMassTransitTestHarness(x => + { + x.UsingRabbitMq((context, cfg) => + { + var rabbitMqOptions = services.GetOptions("RabbitMq"); + var host = rabbitMqOptions.HostName; + + cfg.Host(host, h => + { + h.Username(rabbitMqOptions.UserName); + h.Password(rabbitMqOptions.Password); + }); + cfg.ConfigureEndpoints(context); + }); + }); + + Checkpoint = new Checkpoint {TablesToIgnore = new[] {"__EFMigrationsHistory"}}; + + TestRegistrationServices?.Invoke(services); + }); + + builder.UseDefaultServiceProvider((env, c) => + { + // Handling Captive Dependency Problem + // https://ankitvijay.net/2020/03/17/net-core-and-di-beware-of-captive-dependency/ + // https://blog.ploeh.dk/2014/06/02/captive-dependency/ + if (env.HostingEnvironment.IsEnvironment("test") || env.HostingEnvironment.IsDevelopment()) + c.ValidateScopes = true; + }); + } + + private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider) + { + var httpContextAccessorMock = Substitute.For(); + 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; + } +} \ No newline at end of file diff --git a/src/Services/Identity/tests/IntegrationTest/IntegrationTestFixture.cs b/src/Services/Identity/tests/IntegrationTest/IntegrationTestFixture.cs new file mode 100644 index 0000000..ed7fdc9 --- /dev/null +++ b/src/Services/Identity/tests/IntegrationTest/IntegrationTestFixture.cs @@ -0,0 +1,240 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using BuildingBlocks.Domain.Model; +using BuildingBlocks.EFCore; +using Grpc.Net.Client; +using Identity.Data; +using MassTransit.Testing; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using Serilog; +using Xunit; +using Xunit.Abstractions; + +namespace Integration.Test; + +[CollectionDefinition(nameof(IntegrationTestFixture))] +public class SliceFixtureCollection : ICollectionFixture { } + +public class IntegrationTestFixture : IAsyncLifetime +{ + private readonly CustomWebApplicationFactory _factory; + + public IntegrationTestFixture() + { + // Ref: https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-6.0#basic-tests-with-the-default-webapplicationfactory + _factory = new CustomWebApplicationFactory(); + } + + public IServiceProvider ServiceProvider => _factory.Services; + public IConfiguration Configuration => _factory.Configuration; + public HttpClient HttpClient => _factory.CreateClient(); + public ITestHarness TestHarness => CreateHarness(); + public GrpcChannel Channel => CreateChannel(); + + // 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 void RegisterTestServices(Action services) => _factory.TestRegistrationServices = services; + + public virtual Task InitializeAsync() + { + return Task.CompletedTask; + } + + public virtual async Task DisposeAsync() + { + if (!string.IsNullOrEmpty(Configuration?.GetConnectionString("DefaultConnection"))) + await _factory.Checkpoint.Reset(Configuration?.GetConnectionString("DefaultConnection")); + + await _factory.DisposeAsync(); + } + + public async Task ExecuteScopeAsync(Func action) + { + using var scope = ServiceProvider.CreateScope(); + await action(scope.ServiceProvider); + } + + public async Task ExecuteScopeAsync(Func> action) + { + using var scope = ServiceProvider.CreateScope(); + + var result = await action(scope.ServiceProvider); + + return result; + } + + public Task ExecuteDbContextAsync(Func action) + { + return ExecuteScopeAsync(sp => action(sp.GetService())); + } + + public Task ExecuteDbContextAsync(Func action) + { + return ExecuteScopeAsync(sp => action(sp.GetService()).AsTask()); + } + + public Task ExecuteDbContextAsync(Func action) + { + return ExecuteScopeAsync(sp => action(sp.GetService(), sp.GetService())); + } + + public Task ExecuteDbContextAsync(Func> action) + { + return ExecuteScopeAsync(sp => action(sp.GetService())); + } + + public Task ExecuteDbContextAsync(Func> action) + { + return ExecuteScopeAsync(sp => action(sp.GetService()).AsTask()); + } + + public Task ExecuteDbContextAsync(Func> action) + { + return ExecuteScopeAsync(sp => action(sp.GetService(), sp.GetService())); + } + + public Task InsertAsync(params T[] entities) where T : class + { + return ExecuteDbContextAsync(db => + { + foreach (var entity in entities) db.Set().Add(entity); + + return db.SaveChangesAsync(); + }); + } + + public Task InsertAsync(TEntity entity) where TEntity : class + { + return ExecuteDbContextAsync(db => + { + db.Set().Add(entity); + + return db.SaveChangesAsync(); + }); + } + + public Task InsertAsync(TEntity entity, TEntity2 entity2) + where TEntity : class + where TEntity2 : class + { + return ExecuteDbContextAsync(db => + { + db.Set().Add(entity); + db.Set().Add(entity2); + + return db.SaveChangesAsync(); + }); + } + + public Task InsertAsync(TEntity entity, TEntity2 entity2, TEntity3 entity3) + where TEntity : class + where TEntity2 : class + where TEntity3 : class + { + return ExecuteDbContextAsync(db => + { + db.Set().Add(entity); + db.Set().Add(entity2); + db.Set().Add(entity3); + + return db.SaveChangesAsync(); + }); + } + + public Task InsertAsync(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().Add(entity); + db.Set().Add(entity2); + db.Set().Add(entity3); + db.Set().Add(entity4); + + return db.SaveChangesAsync(); + }); + } + + public Task FindAsync(long id) + where T : class, IEntity + { + return ExecuteDbContextAsync(db => db.Set().FindAsync(id).AsTask()); + } + + public Task SendAsync(IRequest request) + { + return ExecuteScopeAsync(sp => + { + var mediator = sp.GetRequiredService(); + + return mediator.Send(request); + }); + } + + public Task SendAsync(IRequest request) + { + return ExecuteScopeAsync(sp => + { + var mediator = sp.GetRequiredService(); + + return mediator.Send(request); + }); + } + + private IHttpContextAccessor AddHttpContextAccessorMock(IServiceProvider serviceProvider) + { + var httpContextAccessorMock = Substitute.For(); + using var scope = serviceProvider.CreateScope(); + httpContextAccessorMock.HttpContext = new DefaultHttpContext {RequestServices = scope.ServiceProvider}; + + httpContextAccessorMock.HttpContext.Request.Host = new HostString("localhost", 5000); + httpContextAccessorMock.HttpContext.Request.Scheme = "http"; + + return httpContextAccessorMock; + } + + private ITestHarness CreateHarness() + { + var harness = ServiceProvider.GetTestHarness(); + harness.Start().GetAwaiter().GetResult(); + return harness; + } + + private GrpcChannel CreateChannel() + { + return GrpcChannel.ForAddress(HttpClient.BaseAddress!, new GrpcChannelOptions {HttpClient = HttpClient}); + } + + private async Task EnsureDatabaseAsync() + { + using var scope = ServiceProvider.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + var seeders = scope.ServiceProvider.GetServices(); + + await context.Database.MigrateAsync(); + + foreach (var seeder in seeders) await seeder.SeedAllAsync(); + } +} diff --git a/src/Services/Passenger/src/Passenger/Protos/test.proto b/src/Services/Passenger/src/Passenger/Protos/test.proto new file mode 100644 index 0000000..6a4b8cc --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Protos/test.proto @@ -0,0 +1,32 @@ +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package test; + +service Tester { + rpc SayHelloUnary (HelloRequest) returns (HelloReply); + rpc SayHelloServerStreaming (HelloRequest) returns (stream HelloReply); + rpc SayHelloClientStreaming (stream HelloRequest) returns (HelloReply); + rpc SayHelloBidirectionalStreaming (stream HelloRequest) returns (stream HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} diff --git a/src/Services/Passenger/src/Passenger/Services/Greeter.cs b/src/Services/Passenger/src/Passenger/Services/Greeter.cs new file mode 100644 index 0000000..4f6d72f --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Services/Greeter.cs @@ -0,0 +1,38 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using Microsoft.Extensions.Logging; + +namespace Server +{ + public class Greeter : IGreeter + { + private readonly ILogger _logger; + + public Greeter(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public string Greet(string name) + { + _logger.LogInformation($"Creating greeting to {name}"); + return $"Hello {name}"; + } + } +} diff --git a/src/Services/Passenger/src/Passenger/Services/IGreeter.cs b/src/Services/Passenger/src/Passenger/Services/IGreeter.cs new file mode 100644 index 0000000..e8cddf0 --- /dev/null +++ b/src/Services/Passenger/src/Passenger/Services/IGreeter.cs @@ -0,0 +1,25 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +namespace Server +{ + public interface IGreeter + { + string Greet(string name); + } +}