ASP.NET Core에서 IPrincipal 모의


89

단위 테스트를 작성중인 ASP.NET MVC Core 애플리케이션이 있습니다. 작업 방법 중 하나는 일부 기능에 사용자 이름을 사용합니다.

SettingsViewModel svm = _context.MySettings(User.Identity.Name);

단위 테스트에서 분명히 실패합니다. 나는 둘러 보았고 모든 제안은 .NET 4.5에서 HttpContext를 모의합니다. 더 나은 방법이 있다고 확신합니다. IPrincipal을 주입하려고했지만 오류가 발생했습니다. 그리고 나는 이것을 시도했습니다 (절망 때문에).

public IActionResult Index(IPrincipal principal = null) {
    IPrincipal user = principal ?? User;
    SettingsViewModel svm = _context.MySettings(user.Identity.Name);
    return View(svm);
}

그러나 이것은 또한 오류를 던졌습니다. 문서에서도 아무것도 찾을 수 없습니다 ...

답변:


179

컨트롤러의는 액세스되는 관통 컨트롤러의 . 후자는 저장 내에 .User HttpContextControllerContext

사용자를 설정하는 가장 쉬운 방법은 구성된 사용자와 다른 HttpContext를 할당하는 것입니다. 우리는 DefaultHttpContext이 목적을 위해 사용할 수 있습니다 . 그렇게하면 모든 것을 조롱 할 필요가 없습니다. 그런 다음 컨트롤러 컨텍스트 내에서 해당 HttpContext를 사용하고 컨트롤러 인스턴스에 전달합니다.

var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
{
    new Claim(ClaimTypes.Name, "example name"),
    new Claim(ClaimTypes.NameIdentifier, "1"),
    new Claim("custom-claim", "example claim value"),
}, "mock"));

var controller = new SomeController(dependencies…);
controller.ControllerContext = new ControllerContext()
{
    HttpContext = new DefaultHttpContext() { User = user }
};

고유 한 ClaimsIdentity을 만들 때 authenticationType생성자에 명시 적을 전달해야합니다 . 이것은 IsAuthenticated올바르게 작동하는지 확인 합니다 (사용자가 인증되었는지 여부를 확인하기 위해 코드에서 사용하는 경우).


7
제 경우에는 new Claim(ClaimTypes.Name, "1")컨트롤러 사용과 일치하는 것이 었 습니다 user.Identity.Name. 하지만 그렇지 않으면 정확히 내가 달성하려고했던 것입니다 ... Danke schon!
Felix

수 많은 시간을 검색 한 후 마침내 나를 쫓아 낸 게시물이었습니다. 내 핵심 2.0 프로젝트 컨트롤러 방법에서 내가 User.FindFirstValue(ClaimTypes.NameIdentifier);만들고 있던 객체에 userId를 설정하는 데 활용 하고 있었는데 주체가 null이기 때문에 실패했습니다. 이것은 나를 위해 그것을 고쳤습니다. 훌륭한 답변에 감사드립니다!
Timothy Randall

또한 UserManager.GetUserAsync를 작동시키기 위해 수많은 시간을 검색하고 있었는데 이것이 내가 누락 된 링크를 찾은 유일한 곳입니다. 감사! GenericIdentity를 사용하지 않고 클레임을 포함하는 ClaimsIdentity를 설정해야합니다.
에티엔 느 Charland

17

이전 버전에서는 User컨트롤러에서 직접 설정할 수 있었기 때문에 매우 쉬운 단위 테스트가 가능했습니다.

당신의 소스 코드를 보면 ControllerBase 당신은이 것을 알 User에서 추출됩니다 HttpContext.

/// <summary>
/// Gets the <see cref="ClaimsPrincipal"/> for user associated with the executing action.
/// </summary>
public ClaimsPrincipal User => HttpContext?.User;

컨트롤러는 HttpContext비아에 액세스합니다.ControllerContext

/// <summary>
/// Gets the <see cref="Http.HttpContext"/> for the executing action.
/// </summary>
public HttpContext HttpContext => ControllerContext.HttpContext;

이 두 가지는 읽기 전용 속성임을 알 수 있습니다. 좋은 소식은 ControllerContext속성 이 그 가치를 설정할 수 있도록 허용한다는 것입니다.

그래서 목표는 그 물체에 도달하는 것입니다. In Core HttpContext는 추상적이므로 모의하기가 훨씬 쉽습니다.

컨트롤러가 다음과 같다고 가정합니다.

public class MyController : Controller {
    IMyContext _context;

    public MyController(IMyContext context) {
        _context = context;
    }

    public IActionResult Index() {
        SettingsViewModel svm = _context.MySettings(User.Identity.Name);
        return View(svm);
    }

    //...other code removed for brevity 
}

Moq를 사용하면 테스트는 다음과 같이 보일 수 있습니다.

public void Given_User_Index_Should_Return_ViewResult_With_Model() {
    //Arrange 
    var username = "FakeUserName";
    var identity = new GenericIdentity(username, "");

    var mockPrincipal = new Mock<ClaimsPrincipal>();
    mockPrincipal.Setup(x => x.Identity).Returns(identity);
    mockPrincipal.Setup(x => x.IsInRole(It.IsAny<string>())).Returns(true);

    var mockHttpContext = new Mock<HttpContext>();
    mockHttpContext.Setup(m => m.User).Returns(mockPrincipal.Object);

    var model = new SettingsViewModel() {
        //...other code removed for brevity
    };

    var mockContext = new Mock<IMyContext>();
    mockContext.Setup(m => m.MySettings(username)).Returns(model);

    var controller = new MyController(mockContext.Object) {
        ControllerContext = new ControllerContext {
            HttpContext = mockHttpContext.Object
        }
    };

    //Act
    var viewResult = controller.Index() as ViewResult;

    //Assert
    Assert.IsNotNull(viewResult);
    Assert.IsNotNull(viewResult.Model);
    Assert.AreEqual(model, viewResult.Model);
}

3

기존 클래스를 사용하고 필요할 때만 조롱 할 수도 있습니다.

var user = new Mock<ClaimsPrincipal>();
_controller.ControllerContext = new ControllerContext
{
    HttpContext = new DefaultHttpContext
    {
        User = user.Object
    }
};

3

내 경우, 나는의 사용을 만드는 데 필요한 Request.HttpContext.User.Identity.IsAuthenticated, Request.HttpContext.User.Identity.Name및 일부 비즈니스 로직은 컨트롤러의 외부 앉아. 이에 대해 Nkosi, Calin 및 Poke의 답변을 조합하여 사용할 수있었습니다.

var identity = new Mock<IIdentity>();
identity.SetupGet(i => i.IsAuthenticated).Returns(true);
identity.SetupGet(i => i.Name).Returns("FakeUserName");

var mockPrincipal = new Mock<ClaimsPrincipal>();
mockPrincipal.Setup(x => x.Identity).Returns(identity.Object);

var mockAuthHandler = new Mock<ICustomAuthorizationHandler>();
mockAuthHandler.Setup(x => x.CustomAuth(It.IsAny<ClaimsPrincipal>(), ...)).Returns(true).Verifiable();

var controller = new MyController(...);

var mockHttpContext = new Mock<HttpContext>();
mockHttpContext.Setup(m => m.User).Returns(mockPrincipal.Object);

controller.ControllerContext = new ControllerContext();
controller.ControllerContext.HttpContext = new DefaultHttpContext()
{
    User = mockPrincipal.Object
};

var result = controller.Get() as OkObjectResult;
//Assert results

mockAuthHandler.Verify();

2

추상 팩토리 패턴을 구현하려고합니다.

사용자 이름을 제공하기 위해 특별히 공장 용 인터페이스를 만듭니다.

그런 다음을 제공하는 구체적인 클래스 User.Identity.Name와 테스트에 적합한 다른 하드 코딩 된 값을 제공 하는 클래스를 제공합니다 .

그런 다음 프로덕션 코드와 테스트 코드에 따라 적절한 구체적인 클래스를 사용할 수 있습니다. 공장을 매개 변수로 전달하거나 일부 구성 값을 기반으로 올바른 공장으로 전환하려고 할 수 있습니다.

interface IUserNameFactory
{
    string BuildUserName();
}

class ProductionFactory : IUserNameFactory
{
    public BuildUserName() { return User.Identity.Name; }
}

class MockFactory : IUserNameFactory
{
    public BuildUserName() { return "James"; }
}

IUserNameFactory factory;

if(inProductionMode)
{
    factory = new ProductionFactory();
}
else
{
    factory = new MockFactory();
}

SettingsViewModel svm = _context.MySettings(factory.BuildUserName());

감사합니다. 나는 물건에 대해 비슷한 일을 하고 있습니다. 나는 IPrinicpal과 같은 일반적인 것에 대해 "즉시 사용 가능한"무언가가 있기를 바랐습니다. 그러나 분명히 아닙니다!
Felix

또한 User는 ControllerBase의 멤버 변수입니다. 이것이 이전 버전의 ASP.NET에서 사람들이 HttpContext를 조롱하고 거기에서 IPrincipal을 얻은 이유입니다. 하나는 ProductionFactory 같은 독립형 클래스에서 사용자를 얻을 수
펠릭스

1

컨트롤러를 직접 치고 AutoFac과 같은 DI를 사용하고 싶습니다. 이렇게하려면 먼저 등록 ContextController합니다.

var identity = new GenericIdentity("Test User");
var httpContext = new DefaultHttpContext()
{
    User = new GenericPrincipal(identity, null)
};

var context = new ControllerContext { HttpContext = httpContext};
builder.RegisterInstance(context);

다음으로 컨트롤러를 등록 할 때 속성 주입을 활성화합니다.

  builder.RegisterAssemblyTypes(assembly)
                    .Where(t => t.Name.EndsWith("Controller")).PropertiesAutowired();

그런 다음 User.Identity.Name채워지고 컨트롤러에서 메서드를 호출 할 때 특별한 작업을 수행 할 필요가 없습니다.

public async Task<ActionResult<IEnumerable<Employee>>> Get()
{
    var requestedBy = User.Identity?.Name;
    ..................
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.