Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to unit test ServerlessHub #2818

Open
lopezbertoni opened this issue Oct 31, 2024 · 3 comments
Open

How to unit test ServerlessHub #2818

lopezbertoni opened this issue Oct 31, 2024 · 3 comments
Assignees
Labels
area: migration Items related to migration from the in-process model extensions: signal-r

Comments

@lopezbertoni
Copy link

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

@lopezbertoni lopezbertoni added the area: migration Items related to migration from the in-process model label Oct 31, 2024
@Y-Sindo
Copy link
Member

Y-Sindo commented Nov 7, 2024

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
Copy link
Author

@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
Copy link
Member

Y-Sindo commented Nov 8, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: migration Items related to migration from the in-process model extensions: signal-r
Projects
None yet
Development

No branches or pull requests

2 participants