I try to test MassTransit v8 consumer with TestServer by creating whole application with all DI services. I have created TestServer instance with replaced some dependencies but I am most concerned with adding MasstransitTestHarness with consumer.
In test message is sent and consumed by harness correctly, but I can't understand why my consumer can't consume message. TestStartup inherits from my application Startup when I have defined all DI services.
Here is an example of my code:
TestServer program
public class TestProgram
{
private const string API_URL = "http://localhost:5010";
public TestServer Server { get; set; }
public void SetUpTestServer()
{
var host = CreateWebHostBuilder();
Server = new TestServer(host);
}
private IWebHostBuilder CreateWebHostBuilder()
{
return new WebHostBuilder()
.ConfigureTestServices(serviceCollection =>
{
serviceCollection.AddMassTransitTestHarness(cfg =>
{
cfg.AddConsumer<MyConsumer>();
cfg.UsingInMemory((provider, config) =>
{
config.ReceiveEndpoint("MyMessageQueue",
e =>
{
e.Batch<IMyMessage>(b =>
{
b.Consumer<MyConsumer, IMyMessage>(provider);
});
});
});
});
})
.UseStartup<TestStartup>()
.UseUrls(API_URL)
.UseEnvironment("Test");
}
}
Single test
public async Task Test()
{
EndpointConvention.Map<IMyMessage>(new Uri($"queue:MyMessageQueue"));
var program = new TestProgram();
program.SetUpTestServer();
var harness = program.Server.Services.GetTestHarness();
await harness.Start();
try
{
await harness.Bus.Send<IMyMessage>(new MyMessage());
// It's OK
Assert.IsTrue(await harness.Sent.Any<IMyMessage>());
Assert.IsTrue(await harness.Consumed.Any<IMyMessage>());
var consumer = harness.GetConsumerHarness<MyConsumer>();
// It's wrong
Assert.That(await consumer.Consumed.Any<IMyMessage>());
}
finally
{
await harness.Stop();
}
}
Does anyone know why my consumer not consume sent message?
Your configuration is strange, you should follow the configuration guide:
private IWebHostBuilder CreateWebHostBuilder()
{
return new WebHostBuilder()
.ConfigureTestServices(serviceCollection =>
{
serviceCollection.AddMassTransitTestHarness(cfg =>
{
cfg.AddConsumer<MyConsumer>(c =>
c.Options<BatchOptions>(o => o.SetMessageLimit(5).SetTimeLimit(2000)))
.Endpoint(e => e.Name = "MyMessageQueue");;
});
})
.UseStartup<TestStartup>()
.UseUrls(API_URL)
.UseEnvironment("Test");
}
Also, the consumer doesn't consume IMyMessage, it consumes Batch<IMyMessage>, and I'm not entirely sure that consuming batch messages is visible in the harness, I'd have to check and be sure.
Also, as a complete test for comparison:
[TestFixture]
public class When_batch_limit_is_reached
{
[Test]
public async Task Should_deliver_the_batch_to_the_consumer()
{
await using var provider = new ServiceCollection()
.AddMassTransitTestHarness(x =>
{
x.AddConsumer<TestBatchConsumer>();
})
.BuildServiceProvider(true);
var harness = provider.GetTestHarness();
await harness.Start();
await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() });
Assert.That(await harness.Consumed.SelectAsync<BatchItem>().Take(2).Count(), Is.EqualTo(2));
Assert.That(await harness.GetConsumerHarness<TestBatchConsumer>().Consumed.Any<Batch<BatchItem>>(), Is.True);
Assert.That(await harness.Published.Any<BatchResult>(x => x.Context.Message.Count == 2), Is.True);
}
}
I have multiple services and I want to use Microsoft.AspNetCore.Mvc.Testing to test connection between them. I use Microsoft.AspNetCore.Mvc.Testing for integration test for each service and I try to use it for multiple service scenario. But I run into issue how to do it
This is my test:
public class Web1ApplicationFactory : WebApplicationFactory<Startup1>
{
protected override IHostBuilder CreateHostBuilder() =>
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup1>();
});
}
public class Web2ApplicationFactory : WebApplicationFactory<Startup2>
{
protected override IHostBuilder CreateHostBuilder() =>
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup2>();
});
}
[Trait("Category", "E2E")]
public abstract class E2ETestsBase
{
protected readonly HttpClient client1;
protected readonly HttpClient client2;
protected E2ETestsBase()
{
client1 = new Web1ApplicationFactory().CreateClient();
client2 = new Web2ApplicationFactory().CreateClient();
}
}
public class CreateIncidentTests : E2ETestsBase
{
[Fact]
public async Task Should_PassFrom1TO2()
{
// Arrange
var id = MyId.New();
var body = new CreateRequest(id, "sample");
var requestContent = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
// Act
var response1 = await client1.PostAsync("Create", requestContent);
var response2 = await client2.GetAsync($"{id.IdValue}");
// Assert
Assert.Equal(HttpStatusCode.OK, response1.StatusCode);
Assert.Equal("application/json; charset=utf-8", response1.Content.Headers.ContentType?.ToString());
Assert.Equal($"\"{id.IdValue}\"", await response1.Content.ReadAsStringAsync());
Assert.Equal(HttpStatusCode.OK, response2.StatusCode);
}
}
Web1Application post data to Web2Application in some cases via HttpClient.PostAsync in development enviroment to https://localhost:156123. But how to post from Web1Application to Web2Application in tests? There is no port it runs in test process. Is there a way how to test scenario like this? Or I have to use selenium or something like that?
According to release note for MassTransit 7.1.0 version the bus restart should be possible - https://masstransit-project.com/releases/v7.1.0.html#re-start-the-bus-finally.
Unfortunately for MassTransit configured to use RebbitMQ I'm getting exception on message publish after another bus restart. It works fine to and after first restart but after the second restart publish operation throws MassTransit.TransportUnavailableException: The RabbitMQ send transport is stopped.
Below code of simple application (using MassTransit 7.1.3) exposing the problem:
using MassTransit;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
namespace MassTransitRestartIssue
{
class Program
{
static async Task Main(string[] args)
{
IServiceCollection services = new ServiceCollection();
services.AddLogging(logConfig =>
{
logConfig.AddConsole();
});
services.AddMassTransit(config =>
{
config.AddConsumer<TestConsumer>();
config.UsingRabbitMq((ctx, cfg) =>
{
cfg.Host("localhost", "test", h =>
{
h.Username("guest");
h.Password("guest");
});
cfg.ConfigureEndpoints(ctx);
});
});
var serviceProvider = services.BuildServiceProvider();
var busControl = serviceProvider.GetRequiredService<IBusControl>();
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
await busControl.StartAsync();
try
{
await busControl.Publish(new TestEvent("test message 1"));
// restart bus
await busControl.StopAsync();
await busControl.StartAsync();
await busControl.Publish(new TestEvent("test message 2"));
// another restart bus
await busControl.StopAsync();
await busControl.StartAsync();
await busControl.Publish(new TestEvent("test message 3"));
// throws - MassTransit.TransportUnavailableException: The RabbitMQ send transport is stopped: MassTransitRestartIssue:TestEvent
Console.ReadLine();
}
catch (Exception ex)
{
logger.LogError(ex, "error");
}
finally
{
await busControl.StopAsync();
}
}
}
public record TestEvent(string Message)
{
}
public class TestConsumer :
IConsumer<TestEvent>
{
private readonly ILogger _logger;
public TestConsumer(ILogger<TestConsumer> logger)
{
_logger = logger;
}
public Task Consume(ConsumeContext<TestEvent> context)
{
_logger.LogInformation($"Test event consummed - {context.Message.Message}");
return Task.CompletedTask;
}
}
}
Is it a bug or intended behavior? If it intended how can I fix the problem?
Summary
I am trying to add security/authentication to my SignalR hubs, but no matter what I try the client requests keep getting a 403 - Forbidden responses (despite the requests successfully authenticating).
Setup
My project is based on Microsoft's SignalRChat example from:
https://learn.microsoft.com/en-us/aspnet/core/tutorials/signalr?view=aspnetcore-3.1&tabs=visual-studio
Basically I have an ASP.Net Core web application with Razor Pages. The project is targeting .Net Core 3.1.
The client library being used is v3.1.0 of Microsoft's JavaScript client library.
I also referenced their authentication and authorization document for the security side:
https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1
The key difference is rather than using the JWT Bearer middleware, I made my own custom token authentication handler.
Code
chat.js:
"use strict";
var connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub", { accessTokenFactory: () => 'mytoken' })
.configureLogging(signalR.LogLevel.Debug)
.build();
//Disable send button until connection is established
document.getElementById("sendButton").disabled = true;
connection.on("ReceiveMessage", function (user, message) {
var msg = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
var encodedMsg = user + " says " + msg;
var li = document.createElement("li");
li.textContent = encodedMsg;
document.getElementById("messagesList").appendChild(li);
});
connection.start().then(function () {
document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
return console.error(err.toString());
});
document.getElementById("sendButton").addEventListener("click", function (event) {
var user = document.getElementById("userInput").value;
var message = document.getElementById("messageInput").value;
connection.invoke("SendMessage", user, message).catch(function (err) {
return console.error(err.toString());
});
event.preventDefault();
});
Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SignalRChat.Hubs;
using SignalRChat.Security;
namespace SignalRChat
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Found other cases where CORS caused authentication issues, so
// making sure that everything is allowed.
services.AddCors(options =>
{
options.AddPolicy("AllowAny", policy =>
{
policy
.WithOrigins("http://localhost:44312/", "https://localhost:44312/")
.AllowCredentials()
.AllowAnyHeader()
.AllowAnyMethod();
});
});
services
.AddAuthentication()
.AddHubTokenAuthenticationScheme();
services.AddAuthorization(options =>
{
options.AddHubAuthorizationPolicy();
});
services.AddRazorPages();
services.AddSignalR(options =>
{
options.EnableDetailedErrors = true;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapHub<ChatHub>("/chatHub");
});
}
}
}
ChatHub.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using SignalRChat.Security;
using System.Threading.Tasks;
namespace SignalRChat.Hubs
{
[Authorize(HubRequirementDefaults.PolicyName)]
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
}
HubTokenAuthenticationHandler.cs
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
namespace SignalRChat.Security
{
public class HubTokenAuthenticationHandler : AuthenticationHandler<HubTokenAuthenticationOptions>
{
public IServiceProvider ServiceProvider { get; set; }
public HubTokenAuthenticationHandler(
IOptionsMonitor<HubTokenAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IServiceProvider serviceProvider)
: base(options, logger, encoder, clock)
{
ServiceProvider = serviceProvider;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
bool isValid = TryAuthenticate(out AuthenticationTicket ticket, out string message);
if (isValid) return Task.FromResult(AuthenticateResult.Success(ticket));
return Task.FromResult(AuthenticateResult.Fail(message));
}
private bool TryAuthenticate(out AuthenticationTicket ticket, out string message)
{
message = null;
ticket = null;
var token = GetToken();
if (string.IsNullOrEmpty(token))
{
message = "Token is missing";
return false;
}
bool tokenIsValid = token.Equals("mytoken");
if (!tokenIsValid)
{
message = $"Token is invalid: token={token}";
return false;
}
var claims = new[] { new Claim("token", token) };
var identity = new ClaimsIdentity(claims, nameof(HubTokenAuthenticationHandler));
ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), Scheme.Name);
return true;
}
#region Get Token
private string GetToken()
{
string token = Request.Query["access_token"];
if (string.IsNullOrEmpty(token))
{
token = GetTokenFromHeader();
}
return token;
}
private string GetTokenFromHeader()
{
string token = Request.Headers["Authorization"];
if (string.IsNullOrEmpty(token)) return null;
// The Authorization header value should be in the format "Bearer [token_value]"
string[] authorizationParts = token.Split(new char[] { ' ' });
if (authorizationParts == null || authorizationParts.Length < 2) return token;
return authorizationParts[1];
}
#endregion
}
}
HubTokenAuthenticationOptions.cs
using Microsoft.AspNetCore.Authentication;
namespace SignalRChat.Security
{
public class HubTokenAuthenticationOptions : AuthenticationSchemeOptions { }
}
HubTokenAuthenticationDefaults.cs
using Microsoft.AspNetCore.Authentication;
using System;
namespace SignalRChat.Security
{
public static class HubTokenAuthenticationDefaults
{
public const string AuthenticationScheme = "HubTokenAuthentication";
public static AuthenticationBuilder AddHubTokenAuthenticationScheme(this AuthenticationBuilder builder)
{
return AddHubTokenAuthenticationScheme(builder, (options) => { });
}
public static AuthenticationBuilder AddHubTokenAuthenticationScheme(this AuthenticationBuilder builder, Action<HubTokenAuthenticationOptions> configureOptions)
{
return builder.AddScheme<HubTokenAuthenticationOptions, HubTokenAuthenticationHandler>(AuthenticationScheme, configureOptions);
}
}
}
HubRequirement.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace SignalRChat.Security
{
public class HubRequirement : AuthorizationHandler<HubRequirement, HubInvocationContext>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HubRequirement requirement, HubInvocationContext resource)
{
// Authorization logic goes here. Just calling it a success for demo purposes.
context.Succeed(requirement);
return Task.CompletedTask;
}
}
}
HubRequirementDefaults.cs
using Microsoft.AspNetCore.Authentication;
using System;
namespace SignalRChat.Security
{
public static class HubTokenAuthenticationDefaults
{
public const string AuthenticationScheme = "HubTokenAuthentication";
public static AuthenticationBuilder AddHubTokenAuthenticationScheme(this AuthenticationBuilder builder)
{
return AddHubTokenAuthenticationScheme(builder, (options) => { });
}
public static AuthenticationBuilder AddHubTokenAuthenticationScheme(this AuthenticationBuilder builder, Action<HubTokenAuthenticationOptions> configureOptions)
{
return builder.AddScheme<HubTokenAuthenticationOptions, HubTokenAuthenticationHandler>(AuthenticationScheme, configureOptions);
}
}
}
The Results
On the client side, I see the following errors in the browser's developer console:
POST https://localhost:44312/chatHub/negotiate?negotiateVersion=1 403
Error: Failed to complete negotiation with the server: Error
Error: Failed to start the connection: Error
On the server side, all I see is:
SignalRChat.Security.HubTokenAuthenticationHandler: Debug: AuthenticationScheme: HubTokenAuthentication was successfully authenticated.
SignalRChat.Security.HubTokenAuthenticationHandler: Information:
AuthenticationScheme: HubTokenAuthentication was forbidden.
Next Steps
I did see that others had issues with CORS preventing them from security from working, but I believe it usually said that explicitly in the client side errors. Despite that, I added the CORS policies in Startup.cs that I believe should have circumvented that.
I also experimented around with changing the order of service configurations in Startup, but nothing seemed to help.
If I remove the Authorize attribute (i.e. have an unauthenticated hub) everything works fine.
Finally, I found the server side messages to be very interesting in that the authentication succeeded, yet the request was still forbidden.
I'm not really sure where to go from here. Any insights would be most appreciated.
Update
I have been able to debug this a little bit.
By loading system symbols and moving up the call stack, I found my way to Microsoft.AspNetCore.Authorization.Policy.PolicyEvaluator:
As can be seen, the authentication succeeded but apparently the authorization did not. Looking at the requirements, there are two: a DenyAnonymousAuthorizationRequirement and my HubRequirement (which automatically succeeds).
Because the debugger never hit my breakpoint in my HubRequirement class, I am left to assume that the DenyAnonymousAuthorizationRequirement is what is failing. Interesting, because based on the code listing on github (https://github.com/dotnet/aspnetcore/blob/master/src/Security/Authorization/Core/src/DenyAnonymousAuthorizationRequirement.cs) I should be meeting all the requirements:
There is a User defined on the context, the user has an identity, and there are no identities that are unauthenticated.
I have to be missing something, because this isn't adding up.
Turns out the failure was actually happening in my HubRequirement class, and not DenyAnonymousAuthorizationRequirement.
While my HubRequirement class implemented HandleRequirementAsync(), it did not implement HandleAsync(), which is what happened to be what was called instead.
If I update my HubRequirement class to the following, everything works as expected:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace SignalRChat.Security
{
public class HubRequirement : AuthorizationHandler<HubRequirement, HubInvocationContext>, IAuthorizationRequirement
{
public override Task HandleAsync(AuthorizationHandlerContext context)
{
foreach (var requirement in context.PendingRequirements)
{
// TODO: Validate each requirement
}
// Authorization logic goes here. Just calling it a success for demo purposes.
context.Succeed(this);
return Task.CompletedTask;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HubRequirement requirement, HubInvocationContext resource)
{
// Authorization logic goes here. Just calling it a success for demo purposes.
context.Succeed(requirement);
return Task.CompletedTask;
}
}
}
Thank you, saved me a lot of debugging hours!
Looks like the problem is that HandleAsync is also being called with a RouteEndpoint resource for the signalr root and negotiation urls, a case the base class does not handle and since no authorization handler signals success it fails.
public override async Task HandleAsync(AuthorizationHandlerContext context)
{
if (context.Resource is HubInvocationContext)
{
foreach (var req in context.Requirements.OfType<RealtimeHubSecurityAuthorizationHandler>())
{
await HandleRequirementAsync(context, req, (HubInvocationContext)context.Resource);
}
} else if (context.Resource is Microsoft.AspNetCore.Routing.RouteEndpoint) {
//allow signalr root and negotiation url
context.Succeed(this);
}
}
(posted as answer since comment length is limited, sorry)
Context: ASP.NET Core App (1.1).
I try to resolve the IRepository that i've already registered when i need a consumer instance, but it looks like i receive a ObjectDisposedException.
I created a bus factory method that auto activates when i call .Build() method of the ContainerBuilder.
public IServiceProvider ConfigureServices(IServiceCollection services)
{
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterType<EventTypeResolver>().As<IEventTypeResolver>();
containerBuilder.RegisterType<Repository>().As<IRepository>();
containerBuilder.Register(c => EventStoreConnectionFactory.Create())
.As<IEventStoreConnection>();
containerBuilder.Register(BusFactoryMethod).AsSelf().SingleInstance().AutoActivate();
containerBuilder.Populate(services);
return new AutofacServiceProvider(containerBuilder.Build());
}
BusFactoryMethod looks like this:
private IBusControl BusFactoryMethod(IComponentContext componentContext)
{
var busControl = BusConfigurator.Instance.ConfigureBus((cfg, host) =>
{
cfg.ReceiveEndpoint(host, RabbitMQConstants.UserManagementQueue, e =>
{
e.Consumer(() => new CreateUserCommandConsumer(componentContext.Resolve<IRepository>()));
});
});
busControl.Start();
return busControl;
}
So i've found a package MassTransit.AutofacIntegration which already has a RegisterConsumers method.
Then i used Autofac's RegisterGeneric method in order to get a lifetime per message.
containerBuilder.RegisterGeneric(typeof(AutofacConsumerFactory<>))
.WithParameter(new NamedParameter("name", "message"))
.As(typeof(IConsumerFactory<>))
.InstancePerLifetimeScope();
And ended up with the following Module:
public class DefaultModule : Autofac.Module
{
protected override void Load(ContainerBuilder containerBuilder)
{
containerBuilder.Register(c => new EventTypeResolver(typeof(DefaultModule).GetTypeInfo().Assembly)).As<IEventTypeResolver>();
containerBuilder.Register(c => EventStoreConnectionFactory.Create())
.As<IEventStoreConnection>();
containerBuilder.RegisterType<Repository>().As<IRepository>();
containerBuilder.RegisterConsumers(typeof(DefaultModule).GetTypeInfo().Assembly).AsSelf().AsImplementedInterfaces();
containerBuilder.RegisterGeneric(typeof(AutofacConsumerFactory<>))
.WithParameter(new NamedParameter("name", "message"))
.As(typeof(IConsumerFactory<>))
.InstancePerLifetimeScope();
containerBuilder.Register((c) =>
{
var busControl = BusConfigurator.Instance.ConfigureBus((cfg, host) =>
{
ConfigureEndPoints(cfg, host, c);
});
busControl.Start();
return busControl;
}).SingleInstance().AutoActivate();
}
private void ConfigureEndPoints(IRabbitMqBusFactoryConfigurator cfg, IRabbitMqHost host, IComponentContext context)
{
cfg.ReceiveEndpoint(host, RabbitMQConstants.UserManagementQueue, e =>
{
e.Consumer(context.Resolve<IConsumerFactory<CreateUserCommandConsumer>>());
});
}
}
ConfigureServices method is now looking like this:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterModule<DefaultModule>();
containerBuilder.Populate(services);
return new AutofacServiceProvider(containerBuilder.Build());
}