我在尝试为 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
看到
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);
}
}
这个例子显然是微不足道的,但应该仍然适用。该界面只需要适合您的需求。
有一种方法可以通过伪造
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);
}
}
快乐编码!!