├── .gitignore ├── README.md ├── Sample-WAF.sln ├── docker-compose.yml ├── src ├── Sample.Api │ ├── Consumers │ │ ├── NotifyCustomerOrderAcceptedConsumer.cs │ │ └── NotifyCustomerOrderSubmittedConsumer.cs │ ├── Order.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Sample.Api.csproj │ ├── StateMachines │ │ ├── OrderState.cs │ │ ├── OrderStateDefinition.cs │ │ └── OrderStateMachine.cs │ └── appsettings.json ├── Sample.Contracts │ ├── GetOrderStatus.cs │ ├── OrderAccepted.cs │ ├── OrderNotFound.cs │ ├── OrderRejected.cs │ ├── OrderStatus.cs │ ├── OrderSubmissionAccepted.cs │ ├── OrderSubmitted.cs │ ├── OrderValidated.cs │ ├── Sample.Contracts.csproj │ ├── SubmitOrder.cs │ └── ValidateOrder.cs └── Sample.Worker │ ├── Consumers │ └── ValidationConsumer.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Sample.Worker.csproj │ └── appsettings.json └── tests └── Sample.Tests ├── Request_Specs.cs ├── Sample.Tests.csproj ├── SubmitOrder_Specs.cs └── WithoutHarness_Specs.cs /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | *.user 3 | *.dotCover 4 | 5 | .vs 6 | 7 | bin 8 | obj 9 | _ReSharper* 10 | 11 | *.csproj.user 12 | *.resharper.user 13 | *.resharper 14 | *.ReSharper 15 | *.cache 16 | *~ 17 | *.swp 18 | *.bak 19 | *.orig 20 | 21 | **/BenchmarkDotNet.Artifacts/**/* 22 | 23 | # osx noise 24 | .DS_Store 25 | *.DS_Store 26 | *.DotSettings.user 27 | 28 | docs/.vuepress/dist 29 | /node_modules 30 | .vscode 31 | .idea 32 | appsettings.Development.json 33 | 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This sample shows how to use MassTransit's container-based test harness with the `WebApplicationFactory`, without requiring the application under test to know about the test harness. 2 | 3 | The included `docker-compose.yml` can be used to start RabbitMQ and Redis so that the `Sample.Api` project can be run and interactively tested in the browser using the Swagger UI. 4 | 5 | The `Sample.Tests` project uses `AddMassTransitTestHarness` to replace the RabbitMQ transport and Redis saga repository with the in-memory transport and in-memory saga repository, allowing the test to run without any backing services. 6 | 7 | > This requires MassTransit 8.0.3, develop-444 or later. -------------------------------------------------------------------------------- /Sample-WAF.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C13F2FD6-0C59-4125-A84A-8D8A1CF36A4E}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Api", "src\Sample.Api\Sample.Api.csproj", "{1FD972CF-A966-4076-B546-45B2E3D48FE4}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0D17DDB8-4E2D-44A2-8291-6F0A85243B4B}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Tests", "tests\Sample.Tests\Sample.Tests.csproj", "{2FD6E66B-1836-4BEF-A2E9-EC1AB0060B9B}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Worker", "src\Sample.Worker\Sample.Worker.csproj", "{32939793-0B08-474C-BADD-5CAD366D78F9}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Contracts", "src\Sample.Contracts\Sample.Contracts.csproj", "{BF88C1FC-A2A5-498E-A4AF-C56EBE09568B}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(SolutionProperties) = preSolution 24 | HideSolutionNode = FALSE 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {1FD972CF-A966-4076-B546-45B2E3D48FE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {1FD972CF-A966-4076-B546-45B2E3D48FE4}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {1FD972CF-A966-4076-B546-45B2E3D48FE4}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {1FD972CF-A966-4076-B546-45B2E3D48FE4}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {2FD6E66B-1836-4BEF-A2E9-EC1AB0060B9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {2FD6E66B-1836-4BEF-A2E9-EC1AB0060B9B}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {2FD6E66B-1836-4BEF-A2E9-EC1AB0060B9B}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {2FD6E66B-1836-4BEF-A2E9-EC1AB0060B9B}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {32939793-0B08-474C-BADD-5CAD366D78F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {32939793-0B08-474C-BADD-5CAD366D78F9}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {32939793-0B08-474C-BADD-5CAD366D78F9}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {32939793-0B08-474C-BADD-5CAD366D78F9}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {BF88C1FC-A2A5-498E-A4AF-C56EBE09568B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {BF88C1FC-A2A5-498E-A4AF-C56EBE09568B}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {BF88C1FC-A2A5-498E-A4AF-C56EBE09568B}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {BF88C1FC-A2A5-498E-A4AF-C56EBE09568B}.Release|Any CPU.Build.0 = Release|Any CPU 43 | EndGlobalSection 44 | GlobalSection(NestedProjects) = preSolution 45 | {1FD972CF-A966-4076-B546-45B2E3D48FE4} = {C13F2FD6-0C59-4125-A84A-8D8A1CF36A4E} 46 | {2FD6E66B-1836-4BEF-A2E9-EC1AB0060B9B} = {0D17DDB8-4E2D-44A2-8291-6F0A85243B4B} 47 | {32939793-0B08-474C-BADD-5CAD366D78F9} = {C13F2FD6-0C59-4125-A84A-8D8A1CF36A4E} 48 | {BF88C1FC-A2A5-498E-A4AF-C56EBE09568B} = {C13F2FD6-0C59-4125-A84A-8D8A1CF36A4E} 49 | EndGlobalSection 50 | EndGlobal 51 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.3' 2 | 3 | services: 4 | rabbitmq: 5 | image: masstransit/rabbitmq:latest 6 | ports: 7 | - "5672:5672" 8 | - "15672:15672" 9 | redis: 10 | image: redis 11 | ports: 12 | - "6379:6379" -------------------------------------------------------------------------------- /src/Sample.Api/Consumers/NotifyCustomerOrderAcceptedConsumer.cs: -------------------------------------------------------------------------------- 1 | using MassTransit; 2 | using Sample.Contracts; 3 | 4 | namespace Sample.Api.Consumers; 5 | 6 | public class NotifyCustomerOrderAcceptedConsumer : 7 | IConsumer 8 | { 9 | readonly ILogger _logger; 10 | 11 | public NotifyCustomerOrderAcceptedConsumer(ILogger logger) 12 | { 13 | _logger = logger; 14 | } 15 | 16 | public Task Consume(ConsumeContext context) 17 | { 18 | _logger.LogInformation("Order Accepted: {OrderId}", context.Message.OrderId); 19 | return Task.CompletedTask; 20 | } 21 | } -------------------------------------------------------------------------------- /src/Sample.Api/Consumers/NotifyCustomerOrderSubmittedConsumer.cs: -------------------------------------------------------------------------------- 1 | using MassTransit; 2 | using Sample.Contracts; 3 | 4 | namespace Sample.Api.Consumers; 5 | 6 | public class NotifyCustomerOrderSubmittedConsumer : 7 | IConsumer 8 | { 9 | readonly ILogger _logger; 10 | 11 | public NotifyCustomerOrderSubmittedConsumer(ILogger logger) 12 | { 13 | _logger = logger; 14 | } 15 | 16 | public Task Consume(ConsumeContext context) 17 | { 18 | _logger.LogInformation("Order Submitted: {OrderId}", context.Message.OrderId); 19 | return Task.CompletedTask; 20 | } 21 | } -------------------------------------------------------------------------------- /src/Sample.Api/Order.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Api; 2 | 3 | public class Order 4 | { 5 | public Guid OrderId { get; init; } 6 | } -------------------------------------------------------------------------------- /src/Sample.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using MassTransit; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Sample.Api; 4 | using Sample.Api.Consumers; 5 | using Sample.Api.StateMachines; 6 | using Sample.Contracts; 7 | 8 | var builder = WebApplication.CreateBuilder(args); 9 | 10 | builder.Services.AddEndpointsApiExplorer(); 11 | builder.Services.AddSwaggerGen(); 12 | 13 | builder.Services.AddMassTransit(x => 14 | { 15 | x.AddConsumersFromNamespaceContaining(); 16 | 17 | x.AddSagaStateMachine() 18 | .RedisRepository(); 19 | 20 | x.UsingRabbitMq((context, cfg) => 21 | { 22 | cfg.Host("localhost"); 23 | 24 | cfg.ConfigureEndpoints(context); 25 | }); 26 | }); 27 | 28 | var app = builder.Build(); 29 | 30 | app.UseSwagger(); 31 | app.UseSwaggerUI(); 32 | 33 | app.MapPost("/Order", async ([FromBody] Order order, IRequestClient client) => 34 | { 35 | var response = await client.GetResponse(new SubmitOrder(order.OrderId)); 36 | 37 | return Results.Ok(new 38 | { 39 | response.Message.OrderId 40 | }); 41 | }); 42 | 43 | app.MapGet("/Order/{id:guid}", async (Guid id, IRequestClient client) => 44 | { 45 | var response = await client.GetResponse(new GetOrderStatus(id)); 46 | 47 | return response.Is(out Response? order) 48 | ? Results.Ok(order!.Message) 49 | : Results.NotFound(); 50 | }); 51 | 52 | app.Run(); 53 | 54 | public partial class Program 55 | { 56 | } -------------------------------------------------------------------------------- /src/Sample.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:29975", 8 | "sslPort": 44308 9 | } 10 | }, 11 | "profiles": { 12 | "Sample.Api": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "https://localhost:7110;http://localhost:5265", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "IIS Express": { 23 | "commandName": "IISExpress", 24 | "launchBrowser": true, 25 | "launchUrl": "swagger", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Sample.Api/Sample.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Sample.Api/StateMachines/OrderState.cs: -------------------------------------------------------------------------------- 1 | using MassTransit; 2 | 3 | namespace Sample.Api.StateMachines; 4 | 5 | public class OrderState : 6 | SagaStateMachineInstance, 7 | ISagaVersion 8 | { 9 | public string? CurrentState { get; set; } 10 | 11 | public int Version { get; set; } 12 | public Guid CorrelationId { get; set; } 13 | } -------------------------------------------------------------------------------- /src/Sample.Api/StateMachines/OrderStateDefinition.cs: -------------------------------------------------------------------------------- 1 | using MassTransit; 2 | 3 | namespace Sample.Api.StateMachines; 4 | 5 | #pragma warning disable CS8618 6 | public class OrderStateDefinition : 7 | SagaDefinition 8 | { 9 | protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator) 10 | { 11 | endpointConfigurator.UseMessageRetry(r => r.Intervals(100, 200, 500, 1000)); 12 | endpointConfigurator.UseInMemoryOutbox(); 13 | } 14 | } -------------------------------------------------------------------------------- /src/Sample.Api/StateMachines/OrderStateMachine.cs: -------------------------------------------------------------------------------- 1 | using MassTransit; 2 | using Sample.Contracts; 3 | 4 | namespace Sample.Api.StateMachines; 5 | 6 | #pragma warning disable CS8618 7 | public class OrderStateMachine : 8 | MassTransitStateMachine 9 | { 10 | public OrderStateMachine() 11 | { 12 | InstanceState(x => x.CurrentState); 13 | 14 | Event(() => SubmitOrder, x => x.CorrelateById(context => context.Message.OrderId)); 15 | Event(() => OrderStatusRequested, x => 16 | { 17 | x.CorrelateById(context => context.Message.OrderId); 18 | x.OnMissingInstance(m => m.ExecuteAsync(context => context.RespondAsync(new OrderNotFound(context.Message.OrderId)))); 19 | }); 20 | 21 | Request(() => ValidateRequest, x => x.Timeout = TimeSpan.Zero); 22 | 23 | Initially( 24 | When(SubmitOrder) 25 | .TransitionTo(Submitted) 26 | .Publish(x => new OrderSubmitted(x.Saga.CorrelationId)) 27 | .Respond(x => new OrderSubmissionAccepted(x.Saga.CorrelationId)) 28 | .Request(ValidateRequest, x => new ValidateOrder(x.Saga.CorrelationId)) 29 | ); 30 | 31 | During(Submitted, 32 | When(ValidateRequest!.Completed) 33 | .Publish(x => new OrderAccepted(x.Saga.CorrelationId)) 34 | .TransitionTo(Accepted), 35 | When(ValidateRequest.Faulted) 36 | .Publish(x => new OrderRejected(x.Saga.CorrelationId)) 37 | .TransitionTo(Rejected)); 38 | 39 | DuringAny( 40 | When(OrderStatusRequested) 41 | .RespondAsync(async x => new OrderStatus(x.Saga.CorrelationId, (await x.StateMachine.GetState(x)).Name)) 42 | ); 43 | } 44 | 45 | // 46 | // ReSharper disable UnassignedGetOnlyAutoProperty 47 | public State Submitted { get; } 48 | public State Accepted { get; } 49 | public State Rejected { get; } 50 | public Event SubmitOrder { get; } 51 | public Event OrderStatusRequested { get; } 52 | public Request ValidateRequest { get; } 53 | } -------------------------------------------------------------------------------- /src/Sample.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /src/Sample.Contracts/GetOrderStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Contracts; 2 | 3 | public record GetOrderStatus(Guid OrderId); -------------------------------------------------------------------------------- /src/Sample.Contracts/OrderAccepted.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Contracts; 2 | 3 | public record OrderAccepted(Guid OrderId); -------------------------------------------------------------------------------- /src/Sample.Contracts/OrderNotFound.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Contracts; 2 | 3 | public record OrderNotFound(Guid OrderId); -------------------------------------------------------------------------------- /src/Sample.Contracts/OrderRejected.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Contracts; 2 | 3 | public record OrderRejected(Guid OrderId); -------------------------------------------------------------------------------- /src/Sample.Contracts/OrderStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Contracts; 2 | 3 | public record OrderStatus(Guid OrderId, string Status); -------------------------------------------------------------------------------- /src/Sample.Contracts/OrderSubmissionAccepted.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Contracts; 2 | 3 | public record OrderSubmissionAccepted(Guid OrderId); 4 | -------------------------------------------------------------------------------- /src/Sample.Contracts/OrderSubmitted.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Contracts; 2 | 3 | public record OrderSubmitted(Guid OrderId); -------------------------------------------------------------------------------- /src/Sample.Contracts/OrderValidated.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Contracts; 2 | 3 | public record OrderValidated(Guid OrderId); -------------------------------------------------------------------------------- /src/Sample.Contracts/Sample.Contracts.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Sample.Contracts/SubmitOrder.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Contracts; 2 | 3 | public record SubmitOrder(Guid OrderId); -------------------------------------------------------------------------------- /src/Sample.Contracts/ValidateOrder.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Contracts; 2 | 3 | public record ValidateOrder(Guid OrderId); -------------------------------------------------------------------------------- /src/Sample.Worker/Consumers/ValidationConsumer.cs: -------------------------------------------------------------------------------- 1 | using MassTransit; 2 | using Sample.Contracts; 3 | 4 | namespace Sample.Worker.Consumers; 5 | 6 | public class ValidationConsumer : 7 | IConsumer 8 | { 9 | readonly ILogger _logger; 10 | 11 | public ValidationConsumer(ILogger logger) 12 | { 13 | _logger = logger; 14 | } 15 | 16 | public async Task Consume(ConsumeContext context) 17 | { 18 | _logger.LogInformation("Order Validated: {OrderId}", context.Message.OrderId); 19 | 20 | await Task.Delay(1000); 21 | 22 | await context.RespondAsync(new OrderValidated(context.Message.OrderId)); 23 | } 24 | } -------------------------------------------------------------------------------- /src/Sample.Worker/Program.cs: -------------------------------------------------------------------------------- 1 | using MassTransit; 2 | using Sample.Worker.Consumers; 3 | 4 | var host = Host.CreateDefaultBuilder(args) 5 | .ConfigureServices(services => 6 | { 7 | services.AddMassTransit(x => 8 | { 9 | x.AddConsumer(); 10 | 11 | x.UsingRabbitMq((context, cfg) => 12 | { 13 | cfg.Host("localhost"); 14 | 15 | cfg.ConfigureEndpoints(context); 16 | }); 17 | }); 18 | }) 19 | .Build(); 20 | 21 | await host.RunAsync(); -------------------------------------------------------------------------------- /src/Sample.Worker/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Sample.Worker": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "environmentVariables": { 7 | "DOTNET_ENVIRONMENT": "Development" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Sample.Worker/Sample.Worker.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | dotnet-Sample.Worker-2A9ACD61-4466-42F7-BA33-7D0E91AA9340 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Sample.Worker/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/Sample.Tests/Request_Specs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using MassTransit; 4 | using MassTransit.Testing; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using NUnit.Framework; 7 | using Sample.Api.StateMachines; 8 | using Sample.Contracts; 9 | 10 | namespace Sample.Tests; 11 | 12 | [TestFixture] 13 | public class When_request_sent_from_state_machine 14 | { 15 | [Test] 16 | public async Task Should_use_correlation_id_for_request_id() 17 | { 18 | await using var provider = new ServiceCollection() 19 | .AddMassTransitTestHarness(x => 20 | { 21 | x.AddHandler(context => context.RespondAsync(new OrderValidated(context.Message.OrderId))); 22 | 23 | x.AddSagaStateMachine() 24 | .RedisRepository(); 25 | }) 26 | .BuildServiceProvider(true); 27 | 28 | var harness = provider.GetTestHarness(); 29 | 30 | await harness.Start(); 31 | 32 | var client = harness.GetRequestClient(); 33 | 34 | var orderId = Guid.NewGuid(); 35 | 36 | try 37 | { 38 | await client.GetResponse(new SubmitOrder(orderId)); 39 | 40 | var sagaHarness = harness.GetSagaStateMachineHarness(); 41 | 42 | Assert.That(await sagaHarness.Consumed.Any(), Is.True); 43 | 44 | Assert.That(await harness.Published.Any(), Is.True); 45 | } 46 | finally 47 | { 48 | await harness.Stop(); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /tests/Sample.Tests/Sample.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/Sample.Tests/SubmitOrder_Specs.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using System.Threading.Tasks; 3 | using MassTransit; 4 | using MassTransit.Testing; 5 | using Microsoft.AspNetCore.Mvc.Testing; 6 | using NUnit.Framework; 7 | using Sample.Api; 8 | using Sample.Api.StateMachines; 9 | using Sample.Contracts; 10 | 11 | namespace Sample.Tests; 12 | 13 | public class Submitting_an_order 14 | { 15 | [Test] 16 | public async Task Should_have_the_submitted_status() 17 | { 18 | await using var application = new WebApplicationFactory() 19 | .WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddMassTransitTestHarness())); 20 | 21 | var testHarness = application.Services.GetTestHarness(); 22 | 23 | using var client = application.CreateClient(); 24 | 25 | 26 | const string submitOrderUrl = "/Order"; 27 | 28 | var orderId = NewId.NextGuid(); 29 | 30 | var submitOrderResponse = await client.PostAsync(submitOrderUrl, JsonContent.Create(new Order 31 | { 32 | OrderId = orderId 33 | })); 34 | 35 | submitOrderResponse.EnsureSuccessStatusCode(); 36 | var orderStatus = await submitOrderResponse.Content.ReadFromJsonAsync(); 37 | 38 | Assert.That(orderStatus, Is.Not.Null); 39 | Assert.That(orderStatus!.OrderId, Is.EqualTo(orderId)); 40 | 41 | var sagaTestHarness = testHarness.GetSagaStateMachineHarness(); 42 | 43 | Assert.That(await sagaTestHarness.Consumed.Any(x => x.Context.Message.OrderId == orderId), Is.True); 44 | 45 | var sagaExists = await sagaTestHarness.Exists(orderId, x => x.Submitted); 46 | Assert.That(sagaExists.HasValue); 47 | Assert.That(sagaExists!.Value, Is.EqualTo(orderId)); 48 | 49 | var getOrderStatusUrl = $"/Order/{orderId:D}"; 50 | 51 | var orderStatusResponse = await client.GetAsync(getOrderStatusUrl); 52 | orderStatusResponse.EnsureSuccessStatusCode(); 53 | 54 | orderStatus = await orderStatusResponse.Content.ReadFromJsonAsync(); 55 | 56 | Assert.That(orderStatus, Is.Not.Null); 57 | Assert.That(orderStatus!.OrderId, Is.EqualTo(orderId)); 58 | Assert.That(orderStatus.Status, Is.EqualTo(nameof(OrderStateMachine.Submitted))); 59 | } 60 | 61 | [Test] 62 | public async Task Should_have_the_validated_status() 63 | { 64 | await using var application = new WebApplicationFactory() 65 | .WithWebHostBuilder(builder => builder.ConfigureServices(services => 66 | { 67 | services.AddMassTransitTestHarness(x => 68 | { 69 | x.AddHandler(context => context.RespondAsync(new OrderValidated(context.Message.OrderId))); 70 | }); 71 | })); 72 | 73 | var testHarness = application.Services.GetTestHarness(); 74 | 75 | using var client = application.CreateClient(); 76 | 77 | const string submitOrderUrl = "/Order"; 78 | 79 | var orderId = NewId.NextGuid(); 80 | 81 | var submitOrderResponse = await client.PostAsync(submitOrderUrl, JsonContent.Create(new Order 82 | { 83 | OrderId = orderId 84 | })); 85 | 86 | submitOrderResponse.EnsureSuccessStatusCode(); 87 | var orderStatus = await submitOrderResponse.Content.ReadFromJsonAsync(); 88 | 89 | Assert.That(orderStatus, Is.Not.Null); 90 | Assert.That(orderStatus!.OrderId, Is.EqualTo(orderId)); 91 | 92 | var sagaTestHarness = testHarness.GetSagaStateMachineHarness(); 93 | 94 | Assert.That(await sagaTestHarness.Consumed.Any(x => x.Context.Message.OrderId == orderId), Is.True); 95 | 96 | var sagaExists = await sagaTestHarness.Exists(orderId, x => x.Accepted); 97 | Assert.That(sagaExists.HasValue); 98 | Assert.That(sagaExists!.Value, Is.EqualTo(orderId)); 99 | 100 | var getOrderStatusUrl = $"/Order/{orderId:D}"; 101 | 102 | var orderStatusResponse = await client.GetAsync(getOrderStatusUrl); 103 | orderStatusResponse.EnsureSuccessStatusCode(); 104 | 105 | orderStatus = await orderStatusResponse.Content.ReadFromJsonAsync(); 106 | 107 | Assert.That(orderStatus, Is.Not.Null); 108 | Assert.That(orderStatus!.OrderId, Is.EqualTo(orderId)); 109 | Assert.That(orderStatus.Status, Is.EqualTo(nameof(OrderStateMachine.Accepted))); 110 | } 111 | } -------------------------------------------------------------------------------- /tests/Sample.Tests/WithoutHarness_Specs.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using System.Threading.Tasks; 3 | using MassTransit; 4 | using MassTransit.Testing; 5 | using Microsoft.AspNetCore.Mvc.Testing; 6 | using NUnit.Framework; 7 | using Sample.Api; 8 | using Sample.Api.StateMachines; 9 | using Sample.Contracts; 10 | 11 | namespace Sample.Tests; 12 | 13 | public class WithoutHarness_Specs 14 | { 15 | [Test] 16 | public async Task Should_have_the_submitted_status() 17 | { 18 | await using var application = new WebApplicationFactory(); 19 | 20 | using var client = application.CreateClient(); 21 | 22 | const string submitOrderUrl = "/Order"; 23 | 24 | var orderId = NewId.NextGuid(); 25 | 26 | var submitOrderResponse = await client.PostAsync(submitOrderUrl, JsonContent.Create(new Order 27 | { 28 | OrderId = orderId 29 | })); 30 | 31 | submitOrderResponse.EnsureSuccessStatusCode(); 32 | var orderStatus = await submitOrderResponse.Content.ReadFromJsonAsync(); 33 | 34 | Assert.That(orderStatus, Is.Not.Null); 35 | Assert.That(orderStatus!.OrderId, Is.EqualTo(orderId)); 36 | 37 | var getOrderStatusUrl = $"/Order/{orderId:D}"; 38 | 39 | var orderStatusResponse = await client.GetAsync(getOrderStatusUrl); 40 | orderStatusResponse.EnsureSuccessStatusCode(); 41 | 42 | orderStatus = await orderStatusResponse.Content.ReadFromJsonAsync(); 43 | 44 | Assert.That(orderStatus, Is.Not.Null); 45 | Assert.That(orderStatus!.OrderId, Is.EqualTo(orderId)); 46 | Assert.That(orderStatus.Status, Is.EqualTo(nameof(OrderStateMachine.Submitted))); 47 | } 48 | } --------------------------------------------------------------------------------