Skip to content

Commit

Permalink
Add test infra and test for #1390 (#1400)
Browse files Browse the repository at this point in the history
  • Loading branch information
bgavrilMS authored Aug 22, 2021
1 parent 90bd3c4 commit 3bf8f43
Show file tree
Hide file tree
Showing 13 changed files with 2,939 additions and 133 deletions.
1 change: 1 addition & 0 deletions src/Microsoft.Identity.Web/ClientInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Identity.Web.Util;

namespace Microsoft.Identity.Web
{
Expand Down
2,744 changes: 2,626 additions & 118 deletions src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ namespace Microsoft.Identity.Web
/// </summary>
public static class TokenCacheExtensions
{
private static readonly IDictionary<MethodInfo, IServiceProvider> s_serviceProviderFromAction
private static readonly ConcurrentDictionary<MethodInfo, IServiceProvider> s_serviceProviderFromAction
= new ConcurrentDictionary<MethodInfo, IServiceProvider>();

/// <summary>
Expand Down Expand Up @@ -81,20 +81,23 @@ internal static IConfidentialClientApplication AddTokenCaches(
throw new ArgumentNullException(nameof(initializeCaches));
}

// Maintain a dictionary of service providers per `initializeCaches` delegate.
if (!s_serviceProviderFromAction.TryGetValue(initializeCaches.Method, out IServiceProvider? serviceProvider))
// try to reuse existing XYZ cache if AddXYZCache was called before, to simulate ASP.NET Core
var serviceProvider = s_serviceProviderFromAction.GetOrAdd(initializeCaches.Method, (m) =>
{
IHostBuilder hostBuilder = Host.CreateDefaultBuilder()
.ConfigureLogging(logger => { })
.ConfigureServices(services =>
{
initializeCaches(services);
services.AddDataProtection();
});
lock (s_serviceProviderFromAction)
{
IHostBuilder hostBuilder = Host.CreateDefaultBuilder()
.ConfigureLogging(logger => { })
.ConfigureServices(services =>
{
initializeCaches(services);
services.AddDataProtection();
});

serviceProvider = hostBuilder.Build().Services;
s_serviceProviderFromAction[initializeCaches.Method] = serviceProvider;
}
IServiceProvider sp = hostBuilder.Build().Services;
return sp;
}
});

IMsalTokenCacheProvider msalTokenCacheProvider = serviceProvider.GetRequiredService<IMsalTokenCacheProvider>();
msalTokenCacheProvider.Initialize(confidentialClientApp.UserTokenCache);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
// Licensed under the MIT License.

using System;
using System.Globalization;
using System.Text;

namespace Microsoft.Identity.Web
namespace Microsoft.Identity.Web.Util
{
internal static class Base64UrlHelpers
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NSubstitute" Version="4.2.2" />
<PackageReference Include="xunit.assert" Version="2.4.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.164">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.InteropServices;
using Microsoft.Identity.Client;
using Xunit;

namespace Microsoft.Identity.Web.Test.Common.Mocks
{
public class MockHttpClientFactory : IMsalHttpClientFactory, IDisposable
{
/// <inheritdoc />
public void Dispose()
{
// This ensures we only check the mock queue on dispose when we're not in the middle of an
// exception flow. Otherwise, any early assertion will cause this to likely fail
// even though it's not the root cause.
#pragma warning disable CS0618 // Type or member is obsolete - this is non-production code so it's fine
if (Marshal.GetExceptionCode() == 0)
#pragma warning restore CS0618 // Type or member is obsolete
{
string remainingMocks = string.Join(
" ",
_httpMessageHandlerQueue.Select(
h => (h as MockHttpMessageHandler)?.ExpectedUrl ?? string.Empty));

Assert.Empty(_httpMessageHandlerQueue);
}
}

public MockHttpMessageHandler AddMockHandler(MockHttpMessageHandler handler)
{
_httpMessageHandlerQueue.Enqueue(handler);
return handler;
}

private Queue<HttpMessageHandler> _httpMessageHandlerQueue = new Queue<HttpMessageHandler>();

public HttpClient GetHttpClient()
{
HttpMessageHandler messageHandler;

Assert.NotEmpty(_httpMessageHandlerQueue);
messageHandler = _httpMessageHandlerQueue.Dequeue();

var httpClient = new HttpClient(messageHandler);

httpClient.DefaultRequestHeaders.Accept.Clear();
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

return httpClient;
}
}
}
60 changes: 60 additions & 0 deletions tests/Microsoft.Identity.Web.Test.Common/Mocks/MockHttpCreator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Net;
using System.Net.Http;
using Microsoft.Identity.Web.Test.Common;
using Microsoft.Identity.Web.Util;

namespace Microsoft.Identity.Web.Test.Common.Mocks
{
public static class MockHttpCreator
{
private static HttpResponseMessage CreateSuccessfulClientCredentialTokenResponseMessage(string token = "header.payload.signature", string expiry = "3599")
{
return CreateSuccessResponseMessage(
"{\"token_type\":\"Bearer\",\"expires_in\":\"" + expiry + "\",\"client_info\":\"" + CreateClientInfo() + "\",\"access_token\":\"" + token + "\"}");
}

public static HttpResponseMessage CreateSuccessResponseMessage(string successResponse)
{
HttpResponseMessage responseMessage = new HttpResponseMessage(HttpStatusCode.OK);
HttpContent content =
new StringContent(successResponse);
responseMessage.Content = content;
return responseMessage;
}

private static string CreateClientInfo(string uid = TestConstants.Uid, string utid = TestConstants.Utid)
{
return Base64UrlHelpers.Encode("{\"uid\":\"" + uid + "\",\"utid\":\"" + utid + "\"}");
}

public static MockHttpMessageHandler CreateInstanceDiscoveryMockHandler(
string discoveryEndpoint = "https://login.microsoftonline.com/common/discovery/instance",
string content = TestConstants.DiscoveryJsonResponse)
{
return new MockHttpMessageHandler()
{
ExpectedUrl = discoveryEndpoint,
ExpectedMethod = HttpMethod.Get,
ResponseMessage = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(content),
},
};
}

public static MockHttpMessageHandler CreateClientCredentialTokenHandler(
string token = "header.payload.signature", string expiresIn = "3599")
{
var handler = new MockHttpMessageHandler()
{
ExpectedMethod = HttpMethod.Post,
ResponseMessage = CreateSuccessfulClientCredentialTokenResponseMessage(token, expiresIn),
};

return handler;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

namespace Microsoft.Identity.Web.Test.Common.Mocks
{
public class MockHttpMessageHandler : HttpMessageHandler
{
public HttpResponseMessage ResponseMessage { get; set; }
public string ExpectedUrl { get; set; }

public HttpMethod ExpectedMethod { get; set; }

public Exception ExceptionToThrow { get; set; }

/// <summary>
/// Once the http message is executed, this property holds the request message
/// </summary>
public HttpRequestMessage ActualRequestMessage { get; private set; }

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
ActualRequestMessage = request;

if (ExceptionToThrow != null)
{
throw ExceptionToThrow;
}

var uri = request.RequestUri;
if (!string.IsNullOrEmpty(ExpectedUrl))
{
Assert.Equal(
ExpectedUrl,
uri.AbsoluteUri.Split(
new[]
{
'?',
})[0]);
}

Assert.Equal(ExpectedMethod, request.Method);

if (request.Method != HttpMethod.Get && request.Content != null)
{
string postData = request.Content.ReadAsStringAsync().Result;
}

return new TaskFactory().StartNew(() => ResponseMessage, cancellationToken);
}
}
}
37 changes: 37 additions & 0 deletions tests/Microsoft.Identity.Web.Test.Common/TestConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,5 +169,42 @@ public static class TestConstants
public const string ActualIssuer = "actualIssuer";
public const string SecurityToken = "securityToken";
public const string ValidationParameters = "validationParameters";

public const string DiscoveryJsonResponse = @"{
""tenant_discovery_endpoint"":""https://login.microsoftonline.com/tenant/.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""]}
]
}";
}
}
1 change: 1 addition & 0 deletions tests/Microsoft.Identity.Web.Test/Base64UrlHelpersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Text;
using Microsoft.Identity.Web.Util;
using Xunit;

namespace Microsoft.Identity.Web.Test
Expand Down
78 changes: 78 additions & 0 deletions tests/Microsoft.Identity.Web.Test/CacheExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
// Licensed under the MIT License.

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web.Test.Common;
using Microsoft.Identity.Web.Test.Common.Mocks;
using Xunit;

namespace Microsoft.Identity.Web.Test
Expand All @@ -23,6 +26,81 @@ public void InMemoryCacheExtensionsTests()
Assert.NotNull(_confidentialApp.AppTokenCache);
}

[Fact]
// bug: https://github.com/AzureAD/microsoft-identity-web/issues/1390
public async Task InMemoryCacheExtensionsAgainTestsAsync()
{
AuthenticationResult result;
result = await CreateAppAndGetTokenAsync(CacheType.InMemory).ConfigureAwait(false);
Assert.Equal(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource);

result = await CreateAppAndGetTokenAsync(CacheType.InMemory, false, false).ConfigureAwait(false);
Assert.Equal(TokenSource.Cache, result.AuthenticationResultMetadata.TokenSource);

result = await CreateAppAndGetTokenAsync(CacheType.DistributedInMemory, true, false).ConfigureAwait(false);
Assert.Equal(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource);

result = await CreateAppAndGetTokenAsync(CacheType.DistributedInMemory, false, false).ConfigureAwait(false);
Assert.Equal(TokenSource.Cache, result.AuthenticationResultMetadata.TokenSource);
}

private enum CacheType
{
InMemory,
DistributedInMemory,
}

private static async Task<AuthenticationResult> CreateAppAndGetTokenAsync(
CacheType cacheType,
bool addTokenMock = true,
bool addInstanceMock = true)
{
using (MockHttpClientFactory mockHttp = new MockHttpClientFactory())
using (var discoveryHandler = MockHttpCreator.CreateInstanceDiscoveryMockHandler())
using (var tokenHandler = MockHttpCreator.CreateClientCredentialTokenHandler())
{

if (addInstanceMock)
{
mockHttp.AddMockHandler(discoveryHandler);
}

// for when the token is requested from ESTS
if (addTokenMock)
{
mockHttp.AddMockHandler(tokenHandler);
}

var confidentialApp = ConfidentialClientApplicationBuilder
.Create(TestConstants.ClientId)
.WithAuthority(TestConstants.AuthorityCommonTenant)
.WithHttpClientFactory(mockHttp)
.WithClientSecret(TestConstants.ClientSecret)
.Build();

switch (cacheType)
{
case CacheType.InMemory:
confidentialApp.AddInMemoryTokenCache();
break;

case CacheType.DistributedInMemory:
confidentialApp.AddDistributedTokenCache(services =>
{
services.AddDistributedMemoryCache();
services.AddLogging(configure => configure.AddConsole())
.Configure<LoggerFilterOptions>(options => options.MinLevel = Microsoft.Extensions.Logging.LogLevel.Warning);
});
break;
}

var result = await confidentialApp.AcquireTokenForClient(new[] { TestConstants.s_scopeForApp })
.ExecuteAsync().ConfigureAwait(false);

return result;
}
}

[Fact]
public void InMemoryCacheExtensions_NoCca_ThrowsException_Tests()
{
Expand Down
Loading

0 comments on commit 3bf8f43

Please sign in to comment.