4

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@Email.Email", "PasswordHash", "PasswordSalt", status);
    }

    public static IEnumerable TestCases
    {
        get
        {
            //InsertUser_Should_Insert
            yield return new TestCaseData(new InsertUserModel
            {
                FirstName = "FirstName",
                LastName = "LastName",
                StatusId = 1,
                Email = "Email2@Email.Email"
            },
                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 = "Email2@Email.Email"
            },
                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@Email.Email"
            },
                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..

Reft
  • 2,333
  • 5
  • 36
  • 64
  • Create a mock HttpContext which is returned by your mock httpContextAccessor which you then pass it to your constructor of your IdentityService fixture – Mardoxx Jul 14 '17 at 19:53

2 Answers2

6

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

ssmith
  • 8,092
  • 6
  • 52
  • 93
5

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.

Reft
  • 2,333
  • 5
  • 36
  • 64