Mocking IPrincipal di ASP.NET Core

91

Saya memiliki aplikasi ASP.NET MVC Core yang saya tulis untuk pengujian unit. Salah satu metode tindakan menggunakan Nama pengguna untuk beberapa fungsi:

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

yang jelas gagal dalam pengujian unit. Saya melihat sekeliling dan semua saran berasal dari .NET 4.5 untuk meniru HttpContext. Saya yakin ada cara yang lebih baik untuk melakukan itu. Saya mencoba untuk menyuntikkan IPrincipal, tetapi menghasilkan kesalahan; dan saya bahkan mencoba ini (karena putus asa, saya kira):

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

tapi ini juga menimbulkan kesalahan. Tidak dapat menemukan apa pun di dokumen juga ...

Felix
sumber

Jawaban:

182

Pengontrol diakses melalui pengontrol . Yang terakhir disimpan di dalam .User HttpContextControllerContext

Cara termudah untuk menyetel pengguna adalah dengan menetapkan HttpContext yang berbeda dengan pengguna yang dibangun. Kita bisa gunakan DefaultHttpContextuntuk tujuan ini, dengan begitu kita tidak perlu mengejek semuanya. Kemudian kita hanya menggunakan HttpContext itu dalam konteks pengontrol dan meneruskannya ke instance pengontrol:

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

Saat membuat milik Anda sendiri ClaimsIdentity, pastikan untuk meneruskan eksplisit authenticationTypeke konstruktor. Ini memastikan itu IsAuthenticatedakan bekerja dengan benar (jika Anda menggunakannya dalam kode Anda untuk menentukan apakah pengguna diautentikasi).

menyodok
sumber
7
Dalam kasus saya itu new Claim(ClaimTypes.Name, "1")untuk mencocokkan penggunaan pengontrol user.Identity.Name; tapi selain itu, itulah yang saya coba capai ... Danke schon!
Felix
Setelah berjam-jam mencari, ini adalah pos yang akhirnya membuat saya bingung. Dalam metode pengontrol proyek inti 2.0 saya, saya menggunakan User.FindFirstValue(ClaimTypes.NameIdentifier);untuk mengatur userId pada objek yang saya buat dan gagal karena prinsipal adalah null. Ini memperbaikinya untuk saya. Terima kasih atas jawaban yang bagus!
Timothy Randall
Saya juga mencari berjam-jam untuk mendapatkan UserManager.GetUserAsync berfungsi dan ini adalah satu-satunya tempat saya menemukan tautan yang hilang. Terima kasih! Perlu menyiapkan ClaimsIdentity yang berisi Klaim, bukan menggunakan GenericIdentity.
Etienne Charland
17

Dalam versi sebelumnya Anda dapat mengatur Userlangsung pada pengontrol, yang dibuat untuk beberapa pengujian unit yang sangat mudah.

Jika Anda melihat kode sumber untuk ControllerBase Anda akan melihat bahwa Useritu diekstrak dari HttpContext.

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

dan pengontrol mengakses HttpContextviaControllerContext

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

Anda akan melihat bahwa keduanya adalah properti hanya baca. Kabar baiknya adalah bahwa ControllerContextproperti memungkinkan untuk menetapkan nilainya sehingga akan menjadi jalan Anda.

Jadi targetnya adalah mendapatkan objek itu. Dalam Core HttpContextabstrak sehingga lebih mudah untuk diolok-olok.

Dengan asumsi pengontrol suka

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 
}

Menggunakan Moq, tes bisa terlihat seperti ini

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
sumber
3

Ada juga kemungkinan untuk menggunakan kelas yang ada, dan hanya mengejek bila diperlukan.

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

Dalam kasus saya, saya perlu memanfaatkan Request.HttpContext.User.Identity.IsAuthenticated, Request.HttpContext.User.Identity.Namedan beberapa logika bisnis berada di luar kontroler. Saya dapat menggunakan kombinasi jawaban Nkosi, Calin's dan Poke untuk ini:

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
sumber
2

Saya akan menerapkan Pola Pabrik Abstrak.

Buat antarmuka untuk pabrik khusus untuk memberikan nama pengguna.

Kemudian berikan kelas konkret, yang menyediakan User.Identity.Name, dan yang memberikan beberapa nilai kode keras lainnya yang berfungsi untuk pengujian Anda.

Anda kemudian dapat menggunakan kelas beton yang sesuai bergantung pada produksi versus kode pengujian. Mungkin ingin memasukkan pabrik sebagai parameter, atau beralih ke pabrik yang benar berdasarkan beberapa nilai konfigurasi.

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
sumber
Terima kasih. Saya melakukan sesuatu yang serupa untuk objek saya . Saya hanya berharap bahwa untuk hal umum seperti IPrinicpal, akan ada sesuatu yang "di luar kotak". Namun ternyata tidak!
Felix
Selain itu, Pengguna adalah variabel anggota ControllerBase. Itu sebabnya di versi sebelumnya ASP.NET orang mengejek HttpContext, dan mendapatkan IPrincipal dari sana. Seseorang tidak bisa begitu saja mendapatkan Pengguna dari kelas mandiri, seperti ProductionFactory
Felix
1

Saya ingin menekan Controller saya secara langsung dan hanya menggunakan DI seperti AutoFac. Untuk melakukan ini saya mendaftar terlebih dahulu 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);

Selanjutnya saya mengaktifkan injeksi properti saat saya mendaftarkan Pengontrol.

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

Kemudian User.Identity.Namediisi, dan saya tidak perlu melakukan sesuatu yang khusus saat memanggil metode pada Kontroler saya.

public async Task<ActionResult<IEnumerable<Employee>>> Get()
{
    var requestedBy = User.Identity?.Name;
    ..................
Devgig
sumber