การจำลอง IPrincipal ใน ASP.NET Core


91

ฉันมีแอปพลิเคชัน 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);
}

แต่สิ่งนี้ทำให้เกิดข้อผิดพลาดเช่นกัน ไม่พบสิ่งใดในเอกสารเช่นกัน ...

คำตอบ:


182

ตัวควบคุมที่มีการเข้าถึงผ่านของตัวควบคุม หลังถูกเก็บไว้ในไฟล์.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 บนอ็อบเจ็กต์ที่ฉันสร้างและล้มเหลวเนื่องจากหลักการเป็นโมฆะ สิ่งนี้คงที่สำหรับฉัน ขอบคุณสำหรับคำตอบที่ดี!
Timothy Randall

ฉันยังค้นหาชั่วโมงนับไม่ถ้วนเพื่อให้ UserManager.GetUserAsync ทำงานและนี่เป็นที่เดียวที่ฉันพบลิงก์ที่หายไป ขอบคุณ! จำเป็นต้องตั้งค่า ClaimsIdentity ที่มีการอ้างสิทธิ์ไม่ใช่ใช้ GenericIdentity
Etienne 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คุณสมบัติช่วยให้สามารถกำหนดมูลค่าได้เพื่อที่จะเป็นทางเข้าของคุณ

ดังนั้นเป้าหมายคือการไปที่วัตถุนั้น ใน 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 จะมีบางสิ่งที่ "นอกกรอบ" แต่เห็นได้ชัดว่าไม่ใช่!
เฟลิกซ์

นอกจากนี้ User เป็นตัวแปรสมาชิกของ ControllerBase นั่นเป็นเหตุผลที่ใน ASP.NET เวอร์ชันก่อนหน้าผู้คนล้อเลียน HttpContext และได้รับ IPrincipal จากที่นั่น ไม่มีใครสามารถรับ User จากคลาสแบบสแตนด์อโลนได้เช่น ProductionFactory
Felix

1

ฉันต้องการกด Controllers โดยตรงและใช้ DI เช่น AutoFac ในการดำเนินการนี้ฉันลงทะเบียน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.