Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

.NET CORE Testing - Mock IHttpContextAccessor with FakeItEasy

I'm stuck on mocking the IHttpContextAccessor for some web api integration tests. My goal is to be able to mock the IHttpContextAccessor and return NameIdentifier claim and RemoteIpAddress.

Test

public class InsertUser : TestBase
{
    private UserController _userController;

    [OneTimeSetUp]
    public void OneTimeSetUp()
    {
        IStringLocalizer<UserController> localizer = A.Fake<IStringLocalizer<UserController>>();

        _userController = new UserController(localizer, Mapper, UserService, StatusService, IdentityService);
        _userController.ControllerContext = A.Fake<ControllerContext>();
        _userController.ControllerContext.HttpContext = A.Fake<DefaultHttpContext>();

        var fakeClaim = A.Fake<Claim>(x => x.WithArgumentsForConstructor(() => new Claim(ClaimTypes.NameIdentifier, "1")));
        var fakeIdentity = A.Fake<ClaimsPrincipal>();

        A.CallTo(() => fakeIdentity.FindFirst(ClaimTypes.NameIdentifier)).Returns(fakeClaim);
        A.CallTo(() => _userController.ControllerContext.HttpContext.User).Returns(fakeIdentity);

        StatusTypeEntity statusType = ObjectMother.InsertStatusType(StatusTypeEnum.StatusType.User);
        StatusEntity status = ObjectMother.InsertStatus(StatusEnum.Status.Active, statusType);
        ObjectMother.InsertUser("FirstName", "LastName", "[email protected]", "PasswordHash", "PasswordSalt", status);
    }

    public static IEnumerable TestCases
    {
        get
        {
            //InsertUser_Should_Insert
            yield return new TestCaseData(new InsertUserModel
            {
                FirstName = "FirstName",
                LastName = "LastName",
                StatusId = 1,
                Email = "[email protected]"
            },
                1,
                2).SetName("InsertUser_Should_Insert");

            //InsertUser_Should_Not_Insert_When_StatusId_Not_Exist
            yield return new TestCaseData(new InsertUserModel
            {
                FirstName = "FirstName",
                LastName = "LastName",
                StatusId = int.MaxValue,
                Email = "[email protected]"
            },
                1,
                1).SetName("InsertUser_Should_Not_Insert_When_StatusId_Not_Exist");

            //InsertUser_Should_Not_Insert_When_Email_Already_Exist
            yield return new TestCaseData(new InsertUserModel
            {
                FirstName = "FirstName",
                LastName = "LastName",
                StatusId = 1,
                Email = "[email protected]"
            },
                1,
                1).SetName("InsertUser_Should_Not_Insert_When_Email_Already_Exist");
        }
    }

    [Test, TestCaseSource(nameof(TestCases))]
    public async Task Test(InsertUserModel model, int userCountBefore, int userCountAfter)
    {
        //Before
        int resultBefore = Database.User.Count();

        resultBefore.ShouldBe(userCountBefore);

        //Delete
        await _userController.InsertUser(model);

        //After
        int resultAfter = Database.User.Count();

        resultAfter.ShouldBe(userCountAfter);
    }
}

Controller

[Route("api/administration/[controller]")]
[Authorize(Roles = "Administrator")]
public class UserController : Controller
{
    private readonly IStringLocalizer<UserController> _localizer;
    private readonly IMapper _mapper;
    private readonly IUserService _userService;
    private readonly IStatusService _statusService;
    private readonly IIdentityService _identityService;

    public UserController(IStringLocalizer<UserController> localizer,
        IMapper mapper,
        IUserService userService,
        IStatusService statusService,
        IIdentityService identityService)
    {
        _localizer = localizer;
        _mapper = mapper;
        _userService = userService;
        _statusService = statusService;
        _identityService = identityService;
    }

    [HttpPost("InsertUser")]
    public async Task<IActionResult> InsertUser([FromBody] InsertUserModel model)
    {
        if (model == null || !ModelState.IsValid)
        {
            return Ok(new GenericResultModel(_localizer["An_unexpected_error_has_occurred_Please_try_again"]));
        }

        StatusModel status = await _statusService.GetStatus(model.StatusId, StatusTypeEnum.StatusType.User);

        if (status == null)
        {
            return Ok(new GenericResultModel(_localizer["Could_not_find_status"]));
        }

        UserModel userExist = await _userService.GetUser(model.Email);

        if (userExist != null)
        {
            return Ok(new GenericResultModel(_localizer["Email_address_is_already_in_use"]));
        }

        UserModel user = _mapper.Map<InsertUserModel, UserModel>(model);

        var letrTryAndGetUserIdFromNameIdentifier = _identityService.GetUserId();

        user.DefaultIpAddress = _identityService.GetIpAddress();

        //UserModel insertedUser = await _userService.InsertUser(user, model.Password);
        UserModel insertedUser = await _userService.InsertUser(user, "TODO");

        if (insertedUser != null)
        {
            return Ok(new GenericResultModel { Id = insertedUser.Id });
        }

        return Ok(new GenericResultModel(_localizer["Could_not_create_user"]));
    }
}

The important line here is:

var letrTryAndGetUserIdFromNameIdentifier = _identityService.GetUserId();

IdentityService

public class IdentityService : IIdentityService
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public IdentityService(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public int GetUserId()
    {
        if (_httpContextAccessor.HttpContext == null || !Authenticated())
        {
            throw new AuthenticationException("User is not authenticated.");
        }

        ClaimsPrincipal claimsPrincipal = _httpContextAccessor.HttpContext.User;

        string userIdString = claimsPrincipal.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
        int.TryParse(userIdString, out int userIdInt);

        return userIdInt;
    }

    public string GetIpAddress()l
    {
        return _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress.ToString();
    }
}

Fails here:

if (_httpContextAccessor.HttpContext == null || !Authenticated())
{
    throw new AuthenticationException("User is not authenticated.");
}

Currently the _httpContextAccessor.HttpContext is always null. I'm not sure if I'm on the right path here..

like image 418
Reft Avatar asked Dec 19 '22 05:12

Reft


2 Answers

For this kind of test, you probably would be better off writing an integration test that uses the TestHost type, and mocking as little as possible. It will be much simpler, and you'll be able to test filters (like routes and authorization rules), which your current approach doesn't support. You can read more in the docs here: https://learn.microsoft.com/en-us/aspnet/core/testing/integration-testing

I have a good sample showing how to write API tests as part of my MSDN article on ASP.NET Core Filters, here: https://msdn.microsoft.com/en-us/magazine/mt767699.aspx

like image 117
ssmith Avatar answered Jan 03 '23 00:01

ssmith


Modified Test project

var userIdClaim = A.Fake<Claim>(x => x.WithArgumentsForConstructor(() => new Claim(ClaimTypes.NameIdentifier, "1")));

var httpContextAccessor = A.Fake<HttpContextAccessor>();
httpContextAccessor.HttpContext = A.Fake<HttpContext>();
httpContextAccessor.HttpContext.User = A.Fake<ClaimsPrincipal>();
IPAddress ipAddress = IPAddress.Parse("127.0.0.1");
A.CallTo(() => httpContextAccessor.HttpContext.Connection.RemoteIpAddress).Returns(ipAddress);
A.CallTo(() => httpContextAccessor.HttpContext.User.Identity.IsAuthenticated).Returns(true);
A.CallTo(() => httpContextAccessor.HttpContext.User.Claims).Returns(new List<Claim> { userIdClaim });
var identityService = new IdentityService(httpContextAccessor);
_userController = new UserController(localizer, Mapper, UserService, StatusService, identityService);

I'm now able to do in controller:

var claims = HttpContext.User.Claims.ToList();

And identity service:

ClaimsPrincipal claimsPrincipal = _httpContextAccessor.HttpContext.User;

string userIdString = claimsPrincipal.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
int.TryParse(userIdString, out int userIdInt);

return userIdInt;

Please let me now if you think there is a better way for faking HttpContext.

like image 31
Reft Avatar answered Jan 03 '23 01:01

Reft