Azure / azure-functions-dotnet-worker

Azure Functions out-of-process .NET language worker
MIT License
431 stars 184 forks source link

How to unit test ServerlessHub #2818

Open lopezbertoni opened 2 weeks ago

lopezbertoni commented 2 weeks ago

What version of .NET does your existing project use?

.NET 6

What version of .NET are you attempting to target?

.NET 8

Description

After the migration to .NET 8, in order to use SignalR with Azure Functions, it's required to inherit from ServerlessHub. This is no longer unit testable or at least I couldn't find a way to do so.

Here's an example code that I want to unit test.

using DT.Common.Enums.WebSocket;
using DT.Host.Common.Contract.LocalAuthentication;
using DT.Host.SignalRProcessor.Contract;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.SignalRService;
using Microsoft.AspNetCore.SignalR;
using Newtonsoft.Json;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using FromBodyAttribute = Microsoft.Azure.Functions.Worker.Http.FromBodyAttribute;
using System.Diagnostics.CodeAnalysis;

namespace DT.Host.SignalRProcessor.Functions
{
    [SignalRConnection("ConnectionStrings:SignalR")]
    [ExcludeFromCodeCoverage]
    public class SignalRMessageProcessor : ServerlessHub
    {
        private const string HubName = "digitaltrust";

        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);
        }

        private int Authenticate(HttpRequest req)
        {
            req.Headers.TryGetValue("Authorization", out var authHeader);
            if (authHeader.FirstOrDefault() is null)
            {
                return StatusCodes.Status403Forbidden;
            }
            var token = authHeader.First().Trim();
            if (token.StartsWith("basic", StringComparison.OrdinalIgnoreCase))
            {
                try
                {
                    _authenticationManager.Authenticate(token);
                    return StatusCodes.Status200OK;
                }
                catch (UnauthorizedAccessException)
                {
                    return StatusCodes.Status401Unauthorized;
                }

            }
            return StatusCodes.Status401Unauthorized;
        }
}

Any guidance on this is appreciated. I tried mocking IServiceProvider with no luck.

Project configuration and dependencies

No response

Link to a repository that reproduces the issue

No response

Y-Sindo commented 1 week ago

You could have a constructor specially for testing calling the ServerlessHub constructor here. The testing constructor would be like:

  public SignalRMessageProcessor(
      ServiceHubContext serviceHubContext,
      IAuthenticationManager authenticationManager,
      ILogger<SignalRMessageProcessor> logger) : base(serviceHubContext)
  {
      _authenticationManager = authenticationManager;
      _logger = logger;
  }

You can mock the implementation of ServiceHubContext.NegotiateAsync() for testing.

lopezbertoni commented 1 week ago

@Y-Sindo Thanks a lot for the reply.

I don't think I follow your suggestion, the code you provided is injecting the ServiceHubContext but we're really inheriting from the ServerlessHub class. Is your suggestion to change my production code to include the constructor you posted only for testing purposes?

Thanks again for the feedback!

Y-Sindo commented 1 week ago

Is your suggestion to change my production code to include the constructor you posted only for testing purposes?

Yes. This is a common practice that we use a different constructor for testing. You may find many examples in other .NET codes. As the main purpose of IServiceProvider parameter is to provide a ServiceHubContext instance to the serverless hub, it nearly has the same effect to just replace it with a mocked ServiceHubContext in your testing.