如何模拟(最小起订量)IConfidentialClientApplication 已密封设置 AbstractAcquireTokenParameterBuilder?

问题描述 投票:0回答:2

我在尝试为 IConfidentialClientApplication 设置

moq
时遇到以下异常:

System.NotSupportedException:不支持的表达式:... => ....ExecuteAsync() 不可覆盖的成员(这里: AbstractAcquireTokenParameterBuilder.ExecuteAsync) 不得用于设置/验证表达式。

private Mock<IConfidentialClientApplication> _appMock = new Mock<IConfidentialClientApplication>();

[Fact]
public async Task GetAccessTokenResultAsync_WithGoodSetup_ReturnsToken()
{
    // Leverages MSAL AuthenticationResult constructor meant for mocks in test
    var authentication = CreateAuthenticationResult();

    // EXCEPTION THROWN HERE
    _appMock.Setup(_ => _.AcquireTokenForClient(It.IsAny<string[]>()).ExecuteAsync())
        .ReturnsAsync(authentication);

    ... rest of test ...
}

一个

AcquireTokenForClientParameterBuilder
_.AcquireTokenForClient
返回; “使您能够在执行令牌请求之前添加可选参数的构建器”。 这是一个
sealed
,所以我不能轻易地模拟这个棘手的对象。


对于那些好奇的人来说,

CreateAuthenticationResult()
是一种从
Microsoft.Identity.Client.AuthenticationResult
调用签名的方法,它是由 Microsoft 专门添加的,用于存根
AuthenticationResult
,因为它不能被模拟,因为它也是一个密封类。

https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/682

c# unit-testing .net-core moq msal
2个回答
10
投票

看到

AcquireTokenForClientParameterBuilder
是通过外部库提供的,您显然无法修改它以使其更具可测试性。鉴于此,我建议将该代码抽象到您自己的界面后面(某种程度上应用适配器模式进行测试)。

以以下服务/测试为例,说明您当前如何使用

IConfidentialClientApplication
并尝试模拟它(这会导致您看到的相同错误):

public class MyService
{
    private readonly IConfidentialClientApplication _confidentialClientApplication;

    public MyService(IConfidentialClientApplication confidentialClientApplication)
    {
        _confidentialClientApplication = confidentialClientApplication;
    }

    public async Task<string> GetAccessToken(IEnumerable<string> scopes)
    {
        AcquireTokenForClientParameterBuilder tokenBuilder = _confidentialClientApplication.AcquireTokenForClient(scopes);
        AuthenticationResult token = await tokenBuilder.ExecuteAsync();
        return token.AccessToken;
    }
}

public class UnitTest1
{
    [Fact]
    public async Task Test1()
    {
        Mock<IConfidentialClientApplication> _appMock = new Mock<IConfidentialClientApplication>();
        AuthenticationResult authentication = CreateAuthenticationResult("myToken");
        _appMock
            .Setup(_ => _.AcquireTokenForClient(It.IsAny<string[]>()).ExecuteAsync())
            .ReturnsAsync(authentication);

        var myService = new MyService(_appMock.Object);
        string accessToken = await myService.GetAccessToken(new string[] { });

        Assert.Equal("myToken", accessToken);
    }

    private AuthenticationResult CreateAuthenticationResult(string accessToken) => 
        new AuthenticationResult(accessToken, true, null, DateTimeOffset.Now, DateTimeOffset.Now, string.Empty, null, null, null, Guid.Empty);
}

通过引入一个单独的接口,您的代码可以简单地依赖于它,让您可以控制它的使用/测试方式:

public interface IIdentityClientAdapter
{
    Task<string> GetAccessToken(IEnumerable<string> scopes);
}

public class IdentityClientAdapter : IIdentityClientAdapter
{
    private readonly IConfidentialClientApplication _confidentialClientApplication;

    public IdentityClientAdapter(IConfidentialClientApplication confidentialClientApplication)
    {
        _confidentialClientApplication = confidentialClientApplication;
    }

    public async Task<string> GetAccessToken(IEnumerable<string> scopes)
    {
        AcquireTokenForClientParameterBuilder tokenBuilder = _confidentialClientApplication.AcquireTokenForClient(scopes);
        AuthenticationResult token = await tokenBuilder.ExecuteAsync();
        return token.AccessToken;
    }
}

public class MyService
{
    private readonly IIdentityClientAdapter _identityClientAdapter;

    public MyService(IIdentityClientAdapter identityClientAdapter)
    {
        _identityClientAdapter = identityClientAdapter;
    }

    public async Task<string> GetAccessToken(IEnumerable<string> scopes)
    {
        return await _identityClientAdapter.GetAccessToken(scopes);
    }
}

public class UnitTest1
{
    [Fact]
    public async Task Test1()
    {
        Mock<IIdentityClientAdapter> _appMock = new Mock<IIdentityClientAdapter>();
        _appMock
            .Setup(_ => _.GetAccessToken(It.IsAny<string[]>()))
            .ReturnsAsync("myToken");

        var myService = new MyService(_appMock.Object);
        string accessToken = await myService.GetAccessToken(new string[] { });

        Assert.Equal("myToken", accessToken);
    }
}

这个例子显然是微不足道的,但应该仍然适用。该界面只需要适合您的需求。


0
投票

有一种方法可以通过伪造

IConfidentialClientApplication
AcquireTokenForClient
方法来伪造
HttpClient
SendAsync
方法的响应

我有一个单元测试要求,因为两个系统根据标志状态获取身份验证令牌。

所以如果我们看到 AcquireTokenForClient,它会进行两次调用:一次发现调用 (GET) 以首先获取详细信息,然后是实际的令牌调用 (POST),因此要伪造它,我们必须伪造这两个调用。

让我们考虑下面这个测试的实现

public class TokenService
{
    private readonly IConfidentialClientApplication _confidentialClientApplication;

    public TokenService(IConfidentialClientApplication confidentialClientApplication)
    {
        _confidentialClientApplication = confidentialClientApplication;
    }

    public async Task<string> GetToken(string[] scope)
    {
        var token = await _confidentialClientApplication.AcquireTokenForClient(scope).ExecuteAsync();

        return token.AccessToken;
    }
}

这是实现 DelegatingHandler 的类,它将帮助我们伪造 HTTP 响应。对此的解释可在:How to mock HttpClient/Api call response

internal class FakeHttpResponseHandler : DelegatingHandler
{
    private readonly IDictionary<Uri, HttpResponseMessage> fakeServiceResponse;
    public FakeHttpResponseHandler()
    {
        fakeServiceResponse = new Dictionary<Uri, HttpResponseMessage>();
    }

    /// <summary>
    /// Used for adding fake httpResponseMessage for the httpClient operation.
    /// </summary>
    /// <typeparam name="TQueryStringParameter"> query string parameter </typeparam>
    /// <param name="uri">Service end point URL.</param>
    /// <param name="httpResponseMessage"> Response expected when the service called.</param>
    public void AddFakeServiceResponse(Uri uri, HttpResponseMessage httpResponseMessage)
    {
        fakeServiceResponse.Remove(uri);
        fakeServiceResponse.Add(uri, httpResponseMessage);
    }

    // all method in HttpClient call use SendAsync method internally so we are overriding that method here.
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (request.RequestUri != null && fakeServiceResponse.ContainsKey(request.RequestUri))
        {
            var fakeHttpResponseMessage = fakeServiceResponse[request.RequestUri];
            fakeHttpResponseMessage.RequestMessage = request;
            return Task.FromResult(fakeHttpResponseMessage);
        }

        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
        {
            RequestMessage = request,
            Content = new StringContent("No matching fake found")
        });
    }
}

现在,让我们为测试设置一些假值:

string fakeTenantId = Guid.NewGuid().ToString();

string fakeSecret = Guid.NewGuid().ToString();

const string fakeScope = "fakeScope";

string fakeToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";

正如我们之前提到的,

AcquireTokenForClient
进行了两次获取令牌的调用,因此让我们为这两个调用定义端点 url,这是通过
HttpClient
伪造
FakeHttpResponseHandler
所必需的。请注意,如果您设置
IConfidentialClientApplication
的方式与本文中提到的不同,则端点可能会发生变化。

string authEndpoint = Uri.EscapeDataString("https://login.microsoftonline.com/common/oauth2/v2.0/authorize");

string discoverEndpoint = $"https://login.microsoftonline.com:443/common/discovery/instance?api-version=1.1&authorization_endpoint={authEndpoint}";

string tokenEndpoint = $"https://login.microsoftonline.com/common/oauth2/v2.0/token";

现在我们有了所有的 url(键)让我们为每个呼叫设置响应。

HttpResponseMessage fakeDiscoverResponseMessage = new HttpResponseMessage(HttpStatusCode.OK)
{
    RequestMessage = new HttpRequestMessage(HttpMethod.Get, string.Empty),
    Content = new StringContent(
        "{\"tenant_discovery_endpoint\":\"https://login.microsoftonline.com/" + 
        fakeTenantId 
        + "/v2.0/.well-known/openid-configuration\",\"api-version\":\"1.1\",\"metadata\":[{\"preferred_network\":\"login.microsoftonline.com\",\"preferred_cache\":\"login.windows.net\",\"aliases\":[\"login.microsoftonline.com\",\"login.windows.net\",\"login.microsoft.com\",\"sts.windows.net\"]},{\"preferred_network\":\"login.partner.microsoftonline.cn\",\"preferred_cache\":\"login.partner.microsoftonline.cn\",\"aliases\":[\"login.partner.microsoftonline.cn\",\"login.chinacloudapi.cn\"]},{\"preferred_network\":\"login.microsoftonline.de\",\"preferred_cache\":\"login.microsoftonline.de\",\"aliases\":[\"login.microsoftonline.de\"]},{\"preferred_network\":\"login.microsoftonline.us\",\"preferred_cache\":\"login.microsoftonline.us\",\"aliases\":[\"login.microsoftonline.us\",\"login.usgovcloudapi.net\"]},{\"preferred_network\":\"login-us.microsoftonline.com\",\"preferred_cache\":\"login-us.microsoftonline.com\",\"aliases\":[\"login-us.microsoftonline.com\"]}]}")
};

请注意,我们在此响应中将所需的标记设置为

fakeToken

HttpResponseMessage fakeAuthResponseMessage = new HttpResponseMessage(HttpStatusCode.OK)
{
    RequestMessage = new HttpRequestMessage(HttpMethod.Post, string.Empty),
    Content = new StringContent(
        "{\"token_type\":\"Bearer\",\"expires_in\":2588,\"ext_expires_in\":2588,\"access_token\":\"" + 
        fakeToken 
        + "\"}")
};

现在我们有了所有的细节。让我们开始为

FakeHttpResponseHandler
.

中的 Discover 和 Token API 调用设置假货
FakeHttpResponseHandler fakeAuthHandler = new FakeHttpResponseHandler();

fakeAuthHandler.AddFakeServiceResponse(new Uri(discoverEndpoint), fakeDiscoverResponseMessage);
fakeAuthHandler.AddFakeServiceResponse(new Uri(tokenEndpoint), fakeAuthResponseMessage);

现在我们需要模拟

IMsalHttpClientFactory
以返回
HttpClient
,它使用
FakeHttpResponseHandler
,因为我们需要将其传递给
ConfidentialClientApplicationBuilder
以便我们可以使用我们的假
HttpClient
实现。

Mock<IMsalHttpClientFactory> msalHttpClientFactoryMock = new Mock<IMsalHttpClientFactory>();
msalHttpClientFactoryMock.Setup(x => x.GetHttpClient()).Returns(new HttpClient(fakeAuthHandler));

最后,我们需要用

IConfidentialClientApplication
fakeTenantId
fakeSecret
设置
msalHttpClientFactoryMock

IConfidentialClientApplication fakeConfidentialClientApplication = ConfidentialClientApplicationBuilder
            .Create(fakeTenantId)
            .WithClientSecret(fakeSecret)
            .WithHttpClientFactory(msalHttpClientFactoryMock.Object)
            .Build();

就是这样;如果我们将这个

fakeConfidentialClientApplication
传递给
IConfidentialClientApplication
TokenService
,我们将得到想要的输出。完整的测试方法如下,供参考。

public class TokenServiceTest
{
    [Fact]
    public async Task GetFakeTokenByMockingConfidentialClientApplicationAcquireTokenForClient()
    {
        string fakeTenantId = Guid.NewGuid().ToString();

        string fakeSecret = Guid.NewGuid().ToString();

        string fakeToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";

        const string fakeScope = "fakeScope";

        string authEndpoint = Uri.EscapeDataString("https://login.microsoftonline.com/common/oauth2/v2.0/authorize");

        string discoverEndpoint = $"https://login.microsoftonline.com:443/common/discovery/instance?api-version=1.1&authorization_endpoint={authEndpoint}";

        string tokenEndpoint = $"https://login.microsoftonline.com/common/oauth2/v2.0/token";

        HttpResponseMessage fakeDiscoverResponseMessage = new HttpResponseMessage(HttpStatusCode.OK)
        {
            RequestMessage = new HttpRequestMessage(HttpMethod.Get, string.Empty),
            Content = new StringContent(
                "{\"tenant_discovery_endpoint\":\"https://login.microsoftonline.com/" + fakeTenantId + "/v2.0/.well-known/openid-configuration\",\"api-version\":\"1.1\",\"metadata\":[{\"preferred_network\":\"login.microsoftonline.com\",\"preferred_cache\":\"login.windows.net\",\"aliases\":[\"login.microsoftonline.com\",\"login.windows.net\",\"login.microsoft.com\",\"sts.windows.net\"]},{\"preferred_network\":\"login.partner.microsoftonline.cn\",\"preferred_cache\":\"login.partner.microsoftonline.cn\",\"aliases\":[\"login.partner.microsoftonline.cn\",\"login.chinacloudapi.cn\"]},{\"preferred_network\":\"login.microsoftonline.de\",\"preferred_cache\":\"login.microsoftonline.de\",\"aliases\":[\"login.microsoftonline.de\"]},{\"preferred_network\":\"login.microsoftonline.us\",\"preferred_cache\":\"login.microsoftonline.us\",\"aliases\":[\"login.microsoftonline.us\",\"login.usgovcloudapi.net\"]},{\"preferred_network\":\"login-us.microsoftonline.com\",\"preferred_cache\":\"login-us.microsoftonline.com\",\"aliases\":[\"login-us.microsoftonline.com\"]}]}")
        };

        HttpResponseMessage fakeAuthResponseMessage = new HttpResponseMessage(HttpStatusCode.OK)
        {
            RequestMessage = new HttpRequestMessage(HttpMethod.Post, string.Empty),
            Content = new StringContent(
                "{\"token_type\":\"Bearer\",\"expires_in\":2588,\"ext_expires_in\":2588,\"access_token\":\"" + fakeToken + "\"}")
        };

        FakeHttpResponseHandler fakeAuthHandler = new FakeHttpResponseHandler();

        fakeAuthHandler.AddFakeServiceResponse(new Uri(discoverEndpoint), fakeDiscoverResponseMessage);
        fakeAuthHandler.AddFakeServiceResponse(new Uri(tokenEndpoint), fakeAuthResponseMessage);

        Mock<IMsalHttpClientFactory> msalHttpClientFactoryMock = new Mock<IMsalHttpClientFactory>();
        msalHttpClientFactoryMock.Setup(x => x.GetHttpClient()).Returns(new HttpClient(fakeAuthHandler));

        IConfidentialClientApplication fakeConfidentialClientApplication = ConfidentialClientApplicationBuilder.Create(fakeTenantId)
            .WithClientSecret(fakeSecret).WithHttpClientFactory(msalHttpClientFactoryMock.Object).Build();

        var sut = new TokenService(fakeConfidentialClientApplication);

        var response = await sut.GetToken(new string[] { fakeScope });

        Assert.Equal(fakeToken, response);
    }
}

快乐编码!!

© www.soinside.com 2019 - 2024. All rights reserved.