Mocking IPrincipal in ASP.NET Core

Ho un’applicazione ASP.NET MVC Core di cui sto scrivendo i test unitari. Uno dei metodi di azione utilizza il nome utente per alcune funzionalità:

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

che ovviamente fallisce nel test unitario. Mi sono guardato intorno e tutti i suggerimenti provengono da .NET 4.5 per deridere HttpContext. Sono sicuro che c’è un modo migliore per farlo. Ho provato a iniettare IPrincipal, ma ha generato un errore; e ho anche provato questo (per disperazione, suppongo):

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

ma questo ha anche generato un errore. Non ho trovato nulla nemmeno nei documenti …

L’ User del controller è accessibile tramite HttpContext del controller. Quest’ultimo è memorizzato all’interno di ControllerContext .

Il modo più semplice per sostituire l’utente è assegnando un HttpContext diverso con un utente costruito. Possiamo usare DefaultHttpContext per questo scopo, in questo modo non devi prendere in giro tutto:

 var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.NameIdentifier, "1"), new Claim(MyCustomClaim, "example claim value") })); var controller = new SomeController(dependencies…); controller.ControllerContext = new ControllerContext() { HttpContext = new DefaultHttpContext() { User = user } }; 

Nelle versioni precedenti avresti potuto impostare User direttamente sul controller, il che ha reso alcuni test unitari molto semplici.

Se si guarda al codice sorgente di ControllerBase, si noterà che l’ User viene estratto da HttpContext .

 ///  /// Gets or sets the  for user associated with the executing action. ///  public ClaimsPrincipal User { get { return HttpContext?.User; } } 

e il controller accede a HttpContext tramite ControllerContext

 ///  /// Gets the  for the executing action. ///  public HttpContext HttpContext { get { return ControllerContext.HttpContext; } } 

Noterai che queste due sono proprietà di sola lettura. La buona notizia è che la proprietà ControllerContext consente di impostare il suo valore in modo che sia la vostra strada.

Quindi l’objective è raggiungere quell’object. In Core HttpContext è astratto, quindi è molto più facile prendere in giro.

Supponendo un controller come

 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 } 

Usando Moq, un test potrebbe assomigliare a questo

 public void Given_User_Index_Should_Return_ViewResult_With_Model() { //Arrange var username = "FakeUserName"; var identity = new GenericIdentity(username, ""); var mockPrincipal = new Mock(); mockPrincipal.Setup(x => x.Identity).Returns(identity); mockPrincipal.Setup(x => x.IsInRole(It.IsAny())).Returns(true); var mockHttpContext = new Mock(); mockHttpContext.Setup(m => m.User).Returns(mockPrincipal.Object); var model = new SettingsViewModel() { //...other code removed for brevity }; var mockContext = new Mock(); 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); } 

Vorrei cercare di implementare un modello astratto di fabbrica.

Creare un’interfaccia per una fabbrica specificatamente per fornire nomi utente.

Quindi fornire classi concrete, una che fornisce User.Identity.Name e una che fornisce qualche altro valore codificato che User.Identity.Name per i test.

È quindi ansible utilizzare la class concreta appropriata in base alla produzione rispetto al codice di prova. Forse cercando di passare la fabbrica come parametro, o passare alla fabbrica corretta in base ad alcuni valori di configurazione.

 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’è anche la possibilità di utilizzare le classi esistenti e di simulare solo quando necessario.

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