Chế nhạo IPrincipal trong ASP.NET Core


89

Tôi có một ứng dụng ASP.NET MVC Core mà tôi đang viết các bài kiểm tra đơn vị. Một trong những phương thức hành động sử dụng Tên người dùng cho một số chức năng:

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

mà rõ ràng là thất bại trong bài kiểm tra đơn vị. Tôi đã xem xét xung quanh và tất cả các đề xuất đều từ .NET 4.5 để giả lập HttpContext. Tôi chắc chắn có một cách tốt hơn để làm điều đó. Tôi đã cố gắng tiêm IPrincipal, nhưng nó đã báo lỗi; và tôi thậm chí đã thử điều này (tôi cho là vì tuyệt vọng):

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

nhưng điều này cũng gây ra một lỗi. Cũng không tìm thấy gì trong tài liệu ...

Câu trả lời:


179

Bộ điều khiển được truy cập thông qua bộ điều khiển . Sau này được lưu trữ trong .User HttpContextControllerContext

Cách dễ nhất để thiết lập người dùng là gán một HttpContext khác với một người dùng đã xây dựng. Chúng ta có thể sử dụng DefaultHttpContextcho mục đích này, theo cách đó chúng ta không cần phải chế nhạo mọi thứ. Sau đó, chúng tôi chỉ sử dụng HttpContext đó trong ngữ cảnh bộ điều khiển và chuyển nó vào cá thể bộ điều khiển:

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 }
};

Khi tạo của riêng bạn ClaimsIdentity, hãy đảm bảo chuyển một hàm rõ ràng authenticationTypecho hàm tạo. Điều này đảm bảo rằng nó IsAuthenticatedsẽ hoạt động chính xác (trong trường hợp bạn sử dụng nó trong mã của mình để xác định xem người dùng có được xác thực hay không).


7
Trong trường hợp của tôi, nó new Claim(ClaimTypes.Name, "1")phù hợp với việc sử dụng bộ điều khiển của user.Identity.Name; nhưng nếu không thì đó chính xác là những gì tôi đang cố gắng đạt được ... Danke schon!
Felix

Sau vô số giờ tìm kiếm, đây là bài viết cuối cùng đã khiến tôi bình phương. Trong phương pháp bộ điều khiển dự án lõi 2.0 của tôi, tôi đã sử dụng User.FindFirstValue(ClaimTypes.NameIdentifier);để đặt userId trên một đối tượng mà tôi đang tạo và không thành công vì nguyên tắc là null. Điều này đã khắc phục điều đó cho tôi. Cảm ơn vì câu trả lời tuyệt vời!
Timothy Randall

Tôi cũng đã tìm kiếm trong vô số giờ để UserManager.GetUserAsync hoạt động và đây là nơi duy nhất tôi tìm thấy liên kết bị thiếu. Cảm ơn! Cần thiết lập ClaimsIdentity chứa Claim, không sử dụng GenericIdentity.
Etienne Charland

17

Trong các phiên bản trước, bạn có thể đặt Usertrực tiếp trên bộ điều khiển, điều này giúp thực hiện một số bài kiểm tra đơn vị rất dễ dàng.

Nếu bạn nhìn vào mã nguồn của ControllerBase, bạn sẽ nhận thấy rằng nó Userđược trích xuất từ HttpContext.

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

và bộ điều khiển truy cập HttpContextthông quaControllerContext

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

Bạn sẽ nhận thấy rằng hai thuộc tính chỉ đọc. Tin tốt là thuộc ControllerContexttính cho phép thiết lập giá trị của nó để đó sẽ là cách của bạn.

Vì vậy, mục tiêu là để có được đối tượng đó. Trong Core HttpContextlà trừu tượng nên sẽ dễ mô phỏng hơn rất nhiều.

Giả sử một bộ điều khiển như

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 
}

Sử dụng Moq, một bài kiểm tra có thể trông như thế này

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

Ngoài ra còn có khả năng sử dụng các lớp hiện có và chỉ giả lập khi cần thiết.

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

3

Trong trường hợp của tôi, tôi cần phải tận dụng Request.HttpContext.User.Identity.IsAuthenticated, Request.HttpContext.User.Identity.Namevà một số logic kinh doanh ngồi bên ngoài của bộ điều khiển. Tôi đã có thể sử dụng kết hợp câu trả lời của Nkosi, Calin và Poke cho điều này:

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

Tôi muốn triển khai một Mô hình Nhà máy Trừu tượng.

Tạo giao diện cho một nhà máy đặc biệt để cung cấp tên người dùng.

Sau đó, cung cấp các lớp cụ thể, một lớp cung cấp User.Identity.Namevà một lớp cung cấp một số giá trị được mã hóa cứng khác hoạt động cho các thử nghiệm của bạn.

Sau đó, bạn có thể sử dụng loại bê tông thích hợp tùy thuộc vào sản xuất so với mã thử nghiệm. Có lẽ đang tìm cách chuyển nhà máy vào làm tham số hoặc chuyển sang nhà máy chính xác dựa trên một số giá trị cấu hình.

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());

Cảm ơn bạn. Tôi đang làm một cái gì đó tương tự cho các đối tượng của tôi . Tôi chỉ hy vọng rằng đối với thứ phổ biến như IPrinicpal, sẽ có một cái gì đó "ra khỏi hộp". Nhưng dường như, không phải!
Felix

Ngoài ra, Người dùng là biến thành viên của ControllerBase. Đó là lý do tại sao trong các phiên bản trước của ASP.NET, mọi người đã chế giễu HttpContext và lấy IPrincipal từ đó. Người ta không thể chỉ nhận được tài khoản từ một lớp độc lập, như ProductionFactory
Felix

1

Tôi muốn nhấn trực tiếp Bộ điều khiển của mình và chỉ sử dụng DI như AutoFac. Để làm điều này, tôi đăng ký trước 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);

Tiếp theo, tôi bật thuộc tính chèn khi đăng ký Bộ điều khiển.

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

Sau đó User.Identity.Nameđược điền và tôi không cần phải làm gì đặc biệt khi gọi một phương thức trên Bộ điều khiển của mình.

public async Task<ActionResult<IEnumerable<Employee>>> Get()
{
    var requestedBy = User.Identity?.Name;
    ..................
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.