I've worked through the unit testing examples in the SignalR 2 documentation, but now I'd like to test that my hub only notifies a single user in certain situations.
My hub code looks like this:
public class NotificationsHub : Hub
{
public void RaiseAlert(string message)
{
Clients.All.RaiseAlert(message);
}
public void RaiseNotificationAlert(string userId)
{
if (userId == null)
{
// Notify all clients
Clients.All.RaiseAlert("");
return;
}
// Notify only the client for this userId
Clients.User(userId).RaiseAlert("");
}
}
My unit test for checking that all clients are notified looks like this (it's based on the Microsoft example):
[Test]
public void NotifiesAllUsersWhenNoUserIdSpecified()
{
// Based on: https://learn.microsoft.com/vi-vn/aspnet/signalr/overview/testing-and-debugging/unit-testing-signalr-applications
// Arrange
// This is faking the
var mockClients = new Mock<IClientContract>();
mockClients.Setup(m => m.RaiseAlert(It.IsAny<string>())).Verifiable();
// A mock of our SignalR hub's clients
var mockClientConnCtx = new Mock<IHubCallerConnectionContext<dynamic>>();
mockClientConnCtx.Setup(m => m.All).Returns(mockClients.Object);
// Set the hub's connection context to the mock context
var hub = new NotificationsHub
{
Clients = mockClientConnCtx.Object
};
// Action
hub.RaiseNotificationAlert(null);
// Assert
mockClients.Verify(m => m.RaiseAlert(It.IsAny<string>()));
}
What I'm not sure about is how to change the collection of clients represented by the var mockClients = new Mock<IClientContract>() line into a group of individual clients so that I can then test that if I notify user 1, then users 2 and 3 weren't notified?
I found another question about how to unit test groups and one of the answers pointed to the unit tests for the SignalR codebase.
Looking at those examples I worked out that I needed to add mocking of calls to the User method of the mockClients. That ended up looking like this:
public interface IClientContract
{
void RaiseAlert(string message);
}
[Test]
public void NotifiesOnlySpecifiedUserWhenUserIdSent()
{
// Adapted from code here: https://github.com/SignalR/SignalR/blob/dev/tests/Microsoft.AspNet.SignalR.Tests/Server/Hubs/HubFacts.cs
// Arrange
// Set up the individual mock clients
var client1 = new Mock<IClientContract>();
var client2 = new Mock<IClientContract>();
var client3 = new Mock<IClientContract>();
client1.Setup(m => m.RaiseAlert(It.IsAny<string>())).Verifiable();
client2.Setup(m => m.RaiseAlert(It.IsAny<string>())).Verifiable();
client3.Setup(m => m.RaiseAlert(It.IsAny<string>())).Verifiable();
// set the Connection Context to return the mock clients
var mockClients = new Mock<IHubCallerConnectionContext<dynamic>>();
mockClients.Setup(m => m.User("1")).Returns(client1.Object);
mockClients.Setup(m => m.User("2")).Returns(client2.Object);
mockClients.Setup(m => m.User("3")).Returns(client3.Object);
// Assign our mock context to our hub
var hub = new NotificationsHub
{
Clients = mockClients.Object
};
// Act
hub.RaiseNotificationAlert("2");
// Assert
client1.Verify(m => m.RaiseAlert(It.IsAny<string>()), Times.Never);
client2.Verify(m=>m.RaiseAlert(""), Times.Once);
client3.Verify(m => m.RaiseAlert(It.IsAny<string>()), Times.Never);
}
Related
I've got an ASP.NET Core application.
The configuration regarding Openiddict is as follows:
builder.Services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
options.UseEntityFrameworkCore().UseDbContext<IdentityDataContext>();
options.Services.TryAddTransient<OpenIddictQuartzJob>();
// Note: TryAddEnumerable() is used here to ensure the initializer is registered only once.
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<QuartzOptions>, OpenIddictQuartzConfiguration>());
})
// Register the OpenIddict server components.
.AddServer(options =>
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetLogoutEndpointUris("/connect/logout")
.SetTokenEndpointUris("/connect/token")
.SetUserinfoEndpointUris("/connect/userinfo")
// Mark the "email", "profile" and "roles" scopes as supported scopes.
.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles)
// Note: the sample uses the code and refresh token flows but you can enable
// the other flows if you need to support implicit, password or client credentials.
.AllowAuthorizationCodeFlow()
.AllowRefreshTokenFlow()
// Register the signing and encryption credentials.
.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate()
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableUserinfoEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableStatusCodePagesIntegration())
// Register the OpenIddict validation components.
.AddValidation(options =>
{
// Import the configuration from the local OpenIddict server instance.
options.UseLocalServer();
// Register the ASP.NET Core host.
options.UseAspNetCore();
});
builder.Services.ConfigureApplicationCookie(options => options.LoginPath = "/account/auth");
In tests I use a server factory:
public class InMemoryWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup>
where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder) =>
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<IdentityDataContext>))!;
services.Remove(descriptor);
services.AddDbContext<IdentityDataContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
// without this I get a NPE
options.UseOpenIddict();
});
var sp = services.BuildServiceProvider();
using var scope = sp.CreateScope();
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<IdentityDataContext>();
db.Database.EnsureCreated();
});
protected override void ConfigureClient(HttpClient client)
{
base.ConfigureClient(client);
// without this I get Bad request due to Opeiddict filters
client.DefaultRequestHeaders.Host = client.BaseAddress!.Host;
}
}
The test looks like this (taken from here):
[Fact]
public async Task AuthorizedRequestReturnsValue()
{
var client = _factory.WithWebHostBuilder(builder => builder
.ConfigureTestServices(services => services.AddAuthentication("TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("TestScheme", _ => { })))
.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("TestScheme");
var response = await client.GetAsync(new Uri("https://localhost/connect/userinfo"));
// I get Unauthorized here instead
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
The /connect/userinfo is as follows:
[HttpGet("~/connect/userinfo")]
[HttpPost("~/connect/userinfo")]
[IgnoreAntiforgeryToken]
[Produces("application/json")]
public async Task<IActionResult> Userinfo()
{
var user = await _userManager.FindByIdAsync(User.GetClaim(Claims.Subject)!);
if (user is null)
{
return Challenge(
properties: new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidToken,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"The specified access token is bound to an account that no longer exists.",
}),
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
var claims = new Dictionary<string, object>(StringComparer.Ordinal)
{
// Note: the "sub" claim is a mandatory claim and must be included in the JSON response.
[Claims.Subject] = await _userManager.GetUserIdAsync(user),
};
claims[Claims.Email] = (await _userManager.GetEmailAsync(user))!;
claims[Claims.EmailVerified] = await _userManager.IsEmailConfirmedAsync(user);
claims[Claims.Name] = (await _userManager.GetUserNameAsync(user))!;
claims[Claims.PhoneNumber] = (await _userManager.GetPhoneNumberAsync(user))!;
claims[Claims.PhoneNumberVerified] = await _userManager.IsPhoneNumberConfirmedAsync(user);
claims[Claims.Role] = await _userManager.GetRolesAsync(user);
// Note: the complete list of standard claims supported by the OpenID Connect specification
// can be found here: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
return Ok(claims);
}
As far as I understand by default the TestScheme should be used for authentication. But it is not and OpenidDict takes precedence.
Is there a way to make authenticated/authorized requests in integration tests?
P.S. The test was working OK until I added OpenIddict. Before that I used Asp Identity directly for authentication
I didnt think about this but this code is sending the game model to all clients. I need to use the GameID from this controller action and only target the clients watching that game. How do I do that?
Publish Controller Action
public UpdateGameResponse UpdateGame(int gameId)
{
...
var model = Game.Create(XDocument.Load(httpRequest.Files[0].InputStream)).Parse();
GlobalHost.ConnectionManager.GetHubContext<GameCastHub>().Clients.All.receiveUpdates(Newtonsoft.Json.JsonConvert.SerializeObject(model));
}
Hub
[HubName("gamecastHub")]
public class GameCastHub : Hub
{
}
Client
var connected = false;
var gamecastHub = $.connection.gamecastHub;
if (gamecastHub) {
gamecastHub.client.receiveUpdates = function (updates) {
console.log('New updates received');
processUpdates(updates);
};
connectLiveUpdates();
$.connection.hub.connectionSlow(function () {
console.log('Live updates connection running slow');
});
$.connection.hub.disconnected(function () {
connected = false;
console.log('Live updates disconnected');
setTimeout(connectLiveUpdates, 10000);
});
$.connection.hub.reconnecting(function () {
console.log('Live updates reconnecting...');
});
$.connection.hub.reconnected(function () {
connected = false;
console.log('Live updates reconnected');
});
}
I suggest using either the connection Id associated with each connection to the hub or creating groups.
Note: Each GameID must have its own connection to the hub in order to use the connection Id solution.
I prefer to use groups from personal experience but either way can be done.
To create a group in the hub you will need to create a method in your hub class.
public async void setGroup(string groupName){
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
}
Secondly, you will need a JS function on the client side to call the hub function.
$.connection.hub.invoke("setGroup", groupName).catch(err => console.error(err.toString()));
In your case, you can place your gameID as the groupname and then call GlobalHost.ConnectionManager.GetHubContext<GameCastHub>().Clients.Groups(gameID).receiveUpdates(Newtonsoft.Json.JsonConvert.SerializeObject(model));
To retrieve the connection Id:
var _connectionId = $.connection.hub.id;
Then send the connection Id to the server,
and proceed to using the call GlobalHost.ConnectionManager.GetHubContext<GameCastHub>().Clients.Clients.Client(_connectionId).receiveUpdates(Newtonsoft.Json.JsonConvert.SerializeObject(model)); to call that specific connection.
I am trying to use the new User Id provider specified in signalr 2 to send messages to a specific user. When I call the Clients.All method, I see this working as my javascript code gets called from the server and the ui produces some expected text for my test case. However, when I switch to Clients.User the client side code is never called from the server. I followed the code outlined in this example: SignalR - Sending a message to a specific user using (IUserIdProvider) *NEW 2.0.0*.
NotificationHub.cs:
public class NotificationHub : Hub
{
[Authorize]
public void NotifyUser(string userId, int message)
{
Clients.User(userId).DispatchMessage(message);
}
public override Task OnConnected()
{
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
return base.OnDisconnected(stopCalled);
}
public override Task OnReconnected()
{
return base.OnReconnected();
}
}
IUserIdProvider.cs:
public class UserIdProvider : IUserIdProvider
{
MemberService _memberService;
public UserIdProvider()
{
}
public string GetUserId(IRequest request)
{
long UserId = 0;
if (request.User != null && request.User.Identity != null &&
request.User.Identity.Name != null)
{
var currenUser = Task.Run(() => _memberService.FindByUserName(request.User.Identity.Name)).Result;
UserId = currenUser.UserId;
}
return UserId.ToString();
}
}
Startup.cs
HttpConfiguration config = GlobalConfiguration.Configuration;
config.Routes.MapHttpRoute(
"Default2",
"api/{controller}/{action}/{id}",
new { id = RouteParameter.Optional });
config.Routes.MapHttpRoute(
"DefaultApi2",
"api/{controller}/{id}",
new { id = RouteParameter.Optional });
app.Map("/signalr", map =>
{
map.UseCors(CorsOptions.AllowAll);
var idProvider = new UserIdProvider();
GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => idProvider);
map.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()
{
Provider = new QueryStringOAuthBearerAuthenticationProvider()
});
var hubConfiguration = new HubConfiguration
{
};
map.RunSignalR(hubConfiguration);
});
app.MapSignalR();
QuerstringOAuthBearerAuthenticationProvider:
public class QueryStringOAuthBearerAuthenticationProvider
: OAuthBearerAuthenticationProvider
{
public override Task RequestToken(OAuthRequestTokenContext context)
{
if (context == null) throw new ArgumentNullException("context");
// try to find bearer token in a cookie
// (by default OAuthBearerAuthenticationHandler
// only checks Authorization header)
var tokenCookie = context.OwinContext.Request.Cookies["BearerToken"];
if (!string.IsNullOrEmpty(tokenCookie))
context.Token = tokenCookie;
return Task.FromResult<object>(null);
}
}
Do I need to map the user to the connections myself using the IUserIdProvider through the OnConnected, OnDisconnected, etc. or does this happen automatically behind the scenes? Is there someone wrong in my posted code that could be a problem as well? I am running signalr from the same environment as my web api rest services, don't know if this makes a difference and using the default bearer token setup web api is using.
It would be far easier for you to create a group based on the connectionid of the connecting client, in the onConnected event and broadcast to the group that matches the connected id, that way if the client disconnects, when they reconnect they would simply belong to a new group the themselves. Unless of course you are required to have an authenticated user.
Lets say I am setting a value on the http context in my middleware. For example HttpContext.User.
How can test the http context in my unit test. Here is an example of what I am trying to do
Middleware
public class MyAuthMiddleware
{
private readonly RequestDelegate _next;
public MyAuthMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
context.User = SetUser();
await next(context);
}
}
Test
[Fact]
public async Task UserShouldBeAuthenticated()
{
var server = TestServer.Create((app) =>
{
app.UseMiddleware<MyAuthMiddleware>();
});
using(server)
{
var response = await server.CreateClient().GetAsync("/");
// After calling the middleware I want to assert that
// the user in the HttpContext was set correctly
// but how can I access the HttpContext here?
}
}
Following are two approaches you could use:
// Directly test the middleware itself without setting up the pipeline
[Fact]
public async Task Approach1()
{
// Arrange
var httpContext = new DefaultHttpContext();
var authMiddleware = new MyAuthMiddleware(next: (innerHttpContext) => Task.FromResult(0));
// Act
await authMiddleware.Invoke(httpContext);
// Assert
// Note that the User property on DefaultHttpContext is never null and so do
// specific checks for the contents of the principal (ex: claims)
Assert.NotNull(httpContext.User);
var claims = httpContext.User.Claims;
//todo: verify the claims
}
[Fact]
public async Task Approach2()
{
// Arrange
var server = TestServer.Create((app) =>
{
app.UseMiddleware<MyAuthMiddleware>();
app.Run(async (httpContext) =>
{
if(httpContext.User != null)
{
await httpContext.Response.WriteAsync("Claims: "
+ string.Join(
",",
httpContext.User.Claims.Select(claim => string.Format("{0}:{1}", claim.Type, claim.Value))));
}
});
});
using (server)
{
// Act
var response = await server.CreateClient().GetAsync("/");
// Assert
var actual = await response.Content.ReadAsStringAsync();
Assert.Equal("Claims: ClaimType1:ClaimType1-value", actual);
}
}
The RC1 version of asp.net 5/MVC6 makes it possible to set HttpContext manually in Unit Tests, which is awesome!
DemoController demoController = new DemoController();
demoController.ActionContext = new ActionContext();
demoController.ActionContext.HttpContext = new DefaultHttpContext();
demoController.HttpContext.Session = new DummySession();
DefaultHttpContext class is provided by the platform.
DummySession can be just simple class that implements ISession class. This simplifies things a lot, because no more mocking is required.
It would be better if you unit test your middleware class in isolation from the rest of your code.
Since HttpContext class is an abstract class, you can use a mocking framework like Moq (adding "Moq": "4.2.1502.911", as a dependency to your project.json file) to verify that the user property was set.
For example you can write the following test that verifies your middleware Invoke function is setting the User property in the httpContext and calling the next middleware:
[Fact]
public void MyAuthMiddleware_SetsUserAndCallsNextDelegate()
{
//Arrange
var httpContextMock = new Mock<HttpContext>()
.SetupAllProperties();
var delegateMock = new Mock<RequestDelegate>();
var sut = new MyAuthMiddleware(delegateMock.Object);
//Act
sut.Invoke(httpContextMock.Object).Wait();
//Assert
httpContextMock.VerifySet(c => c.User = It.IsAny<ClaimsPrincipal>(), Times.Once);
delegateMock.Verify(next => next(httpContextMock.Object), Times.Once);
}
You could then write additional tests for verifying the user has the expected values, since you will be able to get the setted User object with httpContextMock.Object.User:
Assert.NotNull(httpContextMock.Object.User);
//additional validation, like user claims, id, name, roles
take a look at this post:
Setting HttpContext.Current.Session in a unit test
I think what you need is this.
public static HttpContext FakeHttpContext(string url)
{
var uri = new Uri(url);
var httpRequest = new HttpRequest(string.Empty, uri.ToString(),
uri.Query.TrimStart('?'));
var stringWriter = new StringWriter();
var httpResponse = new HttpResponse(stringWriter);
var httpContext = new HttpContext(httpRequest, httpResponse);
var sessionContainer = new HttpSessionStateContainer("id",
new SessionStateItemCollection(),
new HttpStaticObjectsCollection(),
10, true, HttpCookieMode.AutoDetect,
SessionStateMode.InProc, false);
SessionStateUtility.AddHttpSessionStateToContext(
httpContext, sessionContainer);
return httpContext;
}
Then you can use it like:
request.SetupGet(req => req.Headers).Returns(new NameValueCollection());
HttpContextFactory.Current.Request.Headers.Add(key, value);
When i refresh any client page or any new client arrived, update from connected clients does not reach the new client.
I m using static global connection ids list, and send update to each connection.
I have customized ids in SignalR, and give them my generated UserID like this, and then send update with help of it.
public class CustomUserIdProvider : IUserIdProvider
{
public string GetUserId(IRequest request)
{
var userId = "0";
if (request.User.Identity.IsAuthenticated)
{
var identity = (ClaimsIdentity)request.User.Identity;
userId = identity.FindFirst(ClaimTypes.Sid).Value;
}
return userId.ToString();
}
}
In startup.cs
var idProvider = new CustomUserIdProvider();
GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => idProvider);