Verspotten von IPrincipal in ASP.NET Core

86

Ich habe eine ASP.NET MVC Core-Anwendung, für die ich Unit-Tests schreibe. Eine der Aktionsmethoden verwendet den Benutzernamen für einige Funktionen:

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

was im Unit-Test offensichtlich fehlschlägt. Ich habe mich umgesehen und alle Vorschläge stammen aus .NET 4.5, um HttpContext zu verspotten. Ich bin sicher, es gibt einen besseren Weg, das zu tun. Ich habe versucht, IPrincipal zu injizieren, aber es wurde ein Fehler ausgegeben. und ich habe es sogar versucht (aus Verzweiflung, nehme ich an):

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

aber das warf auch einen Fehler. Konnte auch nichts in den Dokumenten finden ...

Felix
quelle

Antworten:

170

Der Zugriff auf die Steuerung erfolgt über die Steuerung . Letzteres wird gespeichert in der .User HttpContextControllerContext

Der einfachste Weg, den Benutzer festzulegen, besteht darin, einem konstruierten Benutzer einen anderen HttpContext zuzuweisen. Wir können DefaultHttpContextfür diesen Zweck verwenden, so müssen wir nicht alles verspotten. Dann verwenden wir einfach diesen HttpContext in einem Controller-Kontext und übergeben ihn an die Controller-Instanz:

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

Stellen Sie beim Erstellen Ihrer eigenen ClaimsIdentitysicher, dass Sie eine explizite authenticationTypean den Konstruktor übergeben. Dies stellt sicher, dass dies IsAuthenticatedordnungsgemäß funktioniert (falls Sie dies in Ihrem Code verwenden, um festzustellen, ob ein Benutzer authentifiziert ist).

Sack
quelle
7
In meinem Fall war es new Claim(ClaimTypes.Name, "1"), Controller Verwendung von anzupassen user.Identity.Name; aber sonst ist es genau das, was ich erreichen wollte ... Danke schon!
Felix
Nach unzähligen Stunden der Suche war dies der Beitrag, der mich endgültig zum Erliegen brachte. In meiner Core 2.0-Projektcontrollermethode habe ich User.FindFirstValue(ClaimTypes.NameIdentifier);die Benutzer-ID für ein Objekt festgelegt, das ich erstellt habe, und ist fehlgeschlagen, weil der Principal null war. Das hat das für mich behoben. Danke für die tolle Antwort!
Timothy Randall
Ich habe auch unzählige Stunden gesucht, um UserManager.GetUserAsync zum Laufen zu bringen, und dies ist der einzige Ort, an dem ich das fehlende Glied gefunden habe. Vielen Dank! Sie müssen eine ClaimsIdentity einrichten, die einen Claim enthält, und keine GenericIdentity verwenden.
Etienne Charland
16

In früheren Versionen haben Sie möglicherweise Userdirekt auf dem Controller eingestellt, was einige sehr einfache Komponententests ermöglichte.

Wenn Sie sich den Quellcode für ControllerBase ansehen, werden Sie feststellen, dass der aus Userextrahiert wird HttpContext.

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

und die Steuerung greift auf die HttpContextVia zuControllerContext

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

Sie werden feststellen, dass diese beiden Eigenschaften schreibgeschützt sind. Die gute Nachricht ist, dass die ControllerContextEigenschaft es ermöglicht, ihren Wert so einzustellen, dass Sie sich darauf einlassen.

Das Ziel ist es also, an dieses Objekt zu gelangen. In Core HttpContextist abstrakt, so ist es viel einfacher zu verspotten.

Angenommen, ein Controller wie

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 
}

Mit Moq könnte ein Test so aussehen

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);
}
Nkosi
quelle
3

Es besteht auch die Möglichkeit, die vorhandenen Klassen zu verwenden und nur bei Bedarf zu verspotten.

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

In meinem Fall musste ich Gebrauch machen Request.HttpContext.User.Identity.IsAuthenticated, Request.HttpContext.User.Identity.Nameund einige Business - Logik sitzt außerhalb des Controllers. Ich konnte eine Kombination aus Nkosis, Calins und Pokes Antwort verwenden:

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();
Luke
quelle
2

Ich würde versuchen, ein abstraktes Fabrikmuster zu implementieren.

Erstellen Sie eine Schnittstelle für eine Factory, um Benutzernamen bereitzustellen.

Geben Sie dann konkrete Klassen an, eine, die bereitstellt User.Identity.Name, und eine, die einen anderen fest codierten Wert bereitstellt , der für Ihre Tests geeignet ist.

Sie können dann die entsprechende konkrete Klasse verwenden, abhängig von der Produktion im Vergleich zum Testcode. Vielleicht möchten Sie die Factory als Parameter übergeben oder anhand eines Konfigurationswerts zur richtigen Factory wechseln.

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());
James Wood
quelle
Danke dir. Ich mache etwas Ähnliches für meine Objekte. Ich hatte nur gehofft, dass es für so etwas wie IPrinicpal etwas "out of the box" geben würde. Aber anscheinend nicht!
Felix
Darüber hinaus ist Benutzer eine Mitgliedsvariable von ControllerBase. Aus diesem Grund verspotteten die Leute in früheren Versionen von ASP.NET HttpContext und erhielten von dort IPrincipal. Man kann User nicht einfach aus einer eigenständigen Klasse wie ProductionFactory
Felix