Javier Buffa
Javier Buffa

Reputation: 101

Unit testing Azure function with SignalR Serverlesshub

Recently, we updated all our project to .NET 8 and now we need to remake the unit test for our SignalR since are now using Microsoft.Azure.Functions.Worker.SignalRService.ServerlessHub

Thing is this is how our function is setup

[SignalRConnection("ConnectionStrings:SignalR")]
public class SignalRMessageProcessor : ServerlessHub
{
    private readonly IAuthenticationManager _authenticationManager;
    private readonly ILogger<SignalRMessageProcessor> _logger;

    public SignalRMessageProcessor(
        IServiceProvider serviceProvider,
        IAuthenticationManager authenticationManager,
        ILogger<SignalRMessageProcessor> logger) : base(serviceProvider)
    {

        _authenticationManager = authenticationManager;
        _logger = logger;
    }

    [Function("negotiate")]
    public async Task<IActionResult> Negotiate(
        [HttpTrigger("get", Route = "negotiate/{userId}")] HttpRequest req,
        string userId)
    {
        var statusCode = Authenticate(req);
        if (statusCode != StatusCodes.Status200OK)
        {
            return new StatusCodeResult(statusCode);
        }
        _logger.LogInformation($"userId: {userId}; Negotiate Function triggered");

        var negotiateResponse = await NegotiateAsync(new() { UserId = userId });
        
        var response = JsonConvert.DeserializeObject<SignalRConnectionInfo>(negotiateResponse.ToString());

        return new OkObjectResult(response);
    }
}

Authenticate method is not important here because I am doing a mock of IAuthenticationManager since it ours. The problem is the NegotiateAsync since it is a protected method from ServerlessHub.

Is there a way to mock ServerlessHub? Should I create a wrapper class for it so I can just do a passthrough? What is the best solution here?

Upvotes: 1

Views: 56

Answers (1)

Andrew B
Andrew B

Reputation: 977

Unit tests are designed for testing your application logic. But, SignalR isn't part of your application logic; it's just a transport mechanism for the data coming in and out.

So I would flip your approach around. Write your application logic in a class that doesn't rely on ServerlessHub, so that you don't have to mock it at all.

You haven't given any information about how your application logic interacts with (or is separated from) the ServerlessHub. But here's a fictional example that has your negotiation logic moved into a class that can be unit tested.

1. Put your application logic in a service class

Let's create a class with logic that calls your authentication manager and generates some claims.

It also throws an exception if they couldn't be authenticated.

public class MyMessengerLogic
{
    private readonly IAuthenticationManager _authMgr;

    public MyMessengerLogic(IAuthenticationManager authMgr)
    {
        _authMgr = authMgr;
    }

    public async Task<NegotiationOptions> CreateNegotiationAsync(string userId)
    {
        MyUserEntity? user = await _authMgr.AuthenticateAsync(userId);

        if (user == null) throw new MyAuthenticationException("Unauthenticated user");

        return new()
        {
            UserId = user.UserId,
            Claims = GenerateSomeClaims(user)
        };
    }

    private Claims[] GenerateSomeClaims(MyUserEntity user)
    {
        // TODO...
    }
}

2. Simplify your ServerlessHub so it just calls your service class

[SignalRConnection("ConnectionStrings:SignalR")]
public class SignalRMessageProcessor : ServerlessHub
{
    private readonly MyMessengerLogic _service;

    public SignalRMessageProcessor(
        IServiceProvider serviceProvider,
        MyMessengerLogic service) : base(serviceProvider)
    {
        _service = service;
    }

    [Function("negotiate")]
    public async Task<IActionResult> Negotiate(
        [HttpTrigger("get", Route = "negotiate/{userId}")] HttpRequest req,
        string userId)
    {
        try
        {
            // Anything that you want to unit test is now inside this service method:
            var negotiationOptions = await _service.CreateNegotiationAsync(userId);

            // From this point onwards you're just passing it to the Hub
            // (this doesn't need unit testing)
            var negotiateResponse = await NegotiateAsync(negotiationOptions);;
            var response = JsonConvert.DeserializeObject<SignalRConnectionInfo>(negotiateResponse.ToString());
            return new OkObjectResult(response);
        }
        catch (MyAuthenticationException aex)
        {
            // Catch the service exception if unauthenticated.
            return new StatusCodeResult(StatusCodes.Status401Unauthorized);
        }
    }
}

3. Unit test the service class (rather than the hub)

Now that all your testable logic is in a separate class, you can unit test it quite easily... Without worrying about mocking the ServicelessHub.

public class MessengerLogicUnitTests
{
    [Fact]
    public Task Negotiate_WithInvalidUser_ThrowsAuthException()
    {
        var authMgr = new Mock<IAuthenticationManager>();
        authMgr
            .Setup(f => f.AuthenticateAsync(It.IsAny<string>()))
            .Returns(Task.FromResult(null));

        var service = new MyMessengerLogic(authMgr.Object);

        // Check an exception is thrown.
        Assert.ThrowsAsync<MyAuthenticationException>(service.CreateNegotiationAsync("123"));
    }

    [Fact]
    public async Task Negotiate_WithAuthenticatedUser_ReturnsNegotation()
    {
        var authMgr = new Mock<IAuthenticationManager>();
        authMgr
            .Setup(f => f.AuthenticateAsync("123"))
            .Returns(Task.FromResult(new MyUserEntity("123", new Claim[] { /* etc */ })));

        var service = new MyMessengerLogic(authMgr.Object);

        var negotation = await service.CreateNegotiationAsync("123");

        // Check our logic has returned some claims.
        Assert.True(negotation.Claims.Any());
    }
}

Upvotes: 1

Related Questions