-1

I'm trying to add FakeItEasy-based unit tests to a REST API controller of an ASP.NET core app. The public controller methods I need to test call the protected authorization methods implemented in the parent class that rely on the runtime data not available in unit tests. What is the best way to bypass the explicit authorization calls from unit tests? In the other words, how do I make the protected base class method to always succeed?

One option would be to implement authorization calls as a separate interface, but this would require changing the application design, and I would like to make unit tests work without making major changes for now.

Here is the outline of relevant code.

Base controller class:

[ApiController]
public abstract class MicroserviceController: Microsoft.AspNetCore.Mvc.ControllerBase
{
    protected readonly ICorrelationContextAccessor _correlation;
    protected readonly IConfiguration _config;
    protected readonly ILogger _logger;

    protected MicroserviceController
    (
        IConfiguration config,
        ILogger logger,
        ICorrelationContextAccessor correlation
    )
    {
        _config = config;
        _logger = logger;
        _correlation = correlation;
    }

    virtual protected Authorize(string[] scopes, string[] roles)
    {
        // Authorization logic that relies on the ControllerBase methods and properties.
    }

    // More methods.
}

Controller class:

[ApiController]
public class UsersController: MicroserviceController
{
    private IUserService _userService;

    public UsersController
    (
        IConfiguration config,
        ILogger<UsersController> logger, 
        ICorrelationContextAccessor correlation, 
        IUserService userService
    )
    : base(config, logger, correlation)
    {
        _userService = userService;
    }

    [HttpGet]
    public ActionResult<User> GetUser
    (
        string userId
    )
    {

    try
    {
        Authorize(new string[] { "user_read", "user_write", "user_delete" }, null);
    }
    catch (UnauthorizedException ex)
    {
        return Unauthorized(ProcessError(ex));
    }
    catch (Exception ex)
    {
        return BadRequest(ProcessError(ex));
    }

    User user;
    
    try
    {
        user = _userService.GetUserById(userId);
    }
    catch (UnauthorizedException ex)
    {
        return Unauthorized(ProcessError(ex));
    }
    catch (NotFoundException ex)
    {
        return NotFound(ProcessError(ex));
    }
    catch (Exception ex)
    {
        return BadRequest(ProcessError(ex));
    }
    
    return Ok(user);
}

Unit test class:

public class UsersControllerTests
{
    private IConfiguration _config;
    private ILogger<UsersController> _logger; 
    private ICorrelationContextAccessor _correlation; 
    private IUserService _userService;

    public UsersControllerTests()
    {
        _config = A.Fake<IConfiguration>();
        _logger = A.Fake<ILogger<UsersController>>();
        _correlation = A.Fake<ICorrelationContextAccessor>();
        _userService = A.Fake<IUserService>();
    }

    [Fact]
    public void UsersController_GetUser_ReturnOk()
    {
        // Arrange
        UsersController usersController = new UsersController(_config, _logger, _correlation, _userService);

        string userId = "123456789";

        User user = A.Fake<User>();
        A.CallTo(() => _userService.GetUserById(userId)).Returns(user);

        // HOW DO I FORCE the usersController.Authorize(scopes, roles) CALL TO DO NOTHING?

        // Act
        ActionResult<User> result = usersController.GetUser(userId);

        // Assert
        result.Should().NotBeNull();
    }
}

Is it possible to suppress the Authorize calls made by the GetUser call from the usersController object? Or should I start working on the application redesign?

Alek Davis
  • 10,628
  • 2
  • 41
  • 53

1 Answers1

2

HOW DO I FORCE the _userService.Authorize(scopes, roles) CALL TO DO NOTHING?

I assume you mean the usersController.Authorize(scopes, roles) method, since the code contains no mention of _userService.Authorize?

For that, you would need the usersController to be a fake, and then you'd be able to configure the Authorize method as shown here:

A.CallTo(usersControllers)
 .Where(call => call.Method.Name == "Authorize")
 .DoesNothing();

However, that's not a very good approach... Normally you should never have to fake the SUT (subject under test): you're supposed to fake dependencies, not the class you want to test. But with your current design, I see no other option. You can't use FakeItEasy to configure a method of an object that isn't a fake.

Now, if the MicroserviceController.Authorize called another service (e.g. IUserService), you could fake that service and configure what it does. This is the approach I would recommend.

Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
  • Yes, this was a typo, thanks for the correction (I fixed it). And I get your point. I was thinking that it would be a better design if I could decouple the authorization logic from the SUT class. That's what I will probably do. Thanks for explaining the part about not being able to fake methods of the SUT class (for some reason Chat GPT kept insisting that it was possible even gave examples, but I could not make them work). Also appreciate the explanation of SUT. This is my first unit test project so even terminology is new to me. I really appreciate the response. – Alek Davis May 05 '23 at 05:07
  • Btw, if I make the SUT class a fake, how would I test it? I mean, I get the part about mocking the authorization method, but I still need to test the actual method. I'm not going to do this, but I'm wondering how it can be done just in case, unless I misunderstood your point. And I assume you mean I need to declare the UsersController object as a fake, because with, I think, I tried something like your code snippet and got error about the object needing to be fake. – Alek Davis May 05 '23 at 05:13
  • 2
    "Btw, if I make the SUT class a fake, how would I test it? I mean, I get the part about mocking the authorization method, but I still need to test the actual method" That's precisely why you shouldn't fake the SUT: you want to test its actual code! In practice, non-virtual methods won't be faked, so if the method you want to test is not virtual, it will run the actual SUT code. But it's not a good practice... – Thomas Levesque May 05 '23 at 13:56