I have an API that uses IdentityServer4 for token validation. I want to unit test this API with an in-memory TestServer. I'd like to host the IdentityServer in the in-memory TestServer.
I have managed to create a token from the IdentityServer.
This is how far I've come, but I get an error "Unable to obtain configuration from http://localhost:54100/.well-known/openid-configuration"
The Api uses [Authorize]-attribute with different policies. This is what I want to test.
Can this be done, and what am I doing wrong? I have tried to look at the source code for IdentityServer4, but have not come across a similar integration test scenario.
protected IntegrationTestBase() { var startupAssembly = typeof(Startup).GetTypeInfo().Assembly; _contentRoot = SolutionPathUtility.GetProjectPath(@"<my project path>", startupAssembly); Configure(_contentRoot); var orderApiServerBuilder = new WebHostBuilder() .UseContentRoot(_contentRoot) .ConfigureServices(InitializeServices) .UseStartup<Startup>(); orderApiServerBuilder.Configure(ConfigureApp); OrderApiTestServer = new TestServer(orderApiServerBuilder); HttpClient = OrderApiTestServer.CreateClient(); } private void InitializeServices(IServiceCollection services) { var cert = new X509Certificate2(Path.Combine(_contentRoot, "idsvr3test.pfx"), "idsrv3test"); services.AddIdentityServer(options => { options.IssuerUri = "http://localhost:54100"; }) .AddInMemoryClients(Clients.Get()) .AddInMemoryScopes(Scopes.Get()) .AddInMemoryUsers(Users.Get()) .SetSigningCredential(cert); services.AddAuthorization(options => { options.AddPolicy(OrderApiConstants.StoreIdPolicyName, policy => policy.Requirements.Add(new StoreIdRequirement("storeId"))); }); services.AddSingleton<IPersistedGrantStore, InMemoryPersistedGrantStore>(); services.AddSingleton(_orderManagerMock.Object); services.AddMvc(); } private void ConfigureApp(IApplicationBuilder app) { app.UseIdentityServer(); JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); var options = new IdentityServerAuthenticationOptions { Authority = _appsettings.IdentityServerAddress, RequireHttpsMetadata = false, ScopeName = _appsettings.IdentityServerScopeName, AutomaticAuthenticate = false }; app.UseIdentityServerAuthentication(options); app.UseMvc(); }
And in my unit-test:
private HttpMessageHandler _handler; const string TokenEndpoint = "http://localhost/connect/token"; public Test() { _handler = OrderApiTestServer.CreateHandler(); } [Fact] public async Task LeTest() { var accessToken = await GetToken(); HttpClient.SetBearerToken(accessToken); var httpResponseMessage = await HttpClient.GetAsync("stores/11/orders/asdf"); // Fails on this line } private async Task<string> GetToken() { var client = new TokenClient(TokenEndpoint, "client", "secret", innerHttpMessageHandler: _handler); var response = await client.RequestClientCredentialsAsync("TheMOON.OrderApi"); return response.AccessToken; }
API (application programming interface) testing is performed at the message layer without GUI. It is a part of integration testing that determines whether the APIs meet the testers' expectations of functionality, reliability, performance, and security.
Integration Testing is defined as a type of testing where software modules are integrated logically and tested as a group. A typical software project consists of multiple software modules, coded by different programmers.
3.1.JUnit 5 defines an extension interface through which classes can integrate with the JUnit test. We can enable this extension by adding the @ExtendWith annotation to our test classes and specifying the extension class to load.
You were on the right track with the code posted in your initial question.
The IdentityServerAuthenticationOptions object has properties to override the default HttpMessageHandlers it uses for back channel communication.
Once you combine this with the CreateHandler() method on your TestServer object you get:
//build identity server here var idBuilder = new WebBuilderHost(); idBuilder.UseStartup<Startup>(); //... TestServer identityTestServer = new TestServer(idBuilder); var identityServerClient = identityTestServer.CreateClient(); var token = //use identityServerClient to get Token from IdentityServer //build Api TestServer var options = new IdentityServerAuthenticationOptions() { Authority = "http://localhost:5001", // IMPORTANT PART HERE JwtBackChannelHandler = identityTestServer.CreateHandler(), IntrospectionDiscoveryHandler = identityTestServer.CreateHandler(), IntrospectionBackChannelHandler = identityTestServer.CreateHandler() }; var apiBuilder = new WebHostBuilder(); apiBuilder.ConfigureServices(c => c.AddSingleton(options)); //build api server here var apiClient = new TestServer(apiBuilder).CreateClient(); apiClient.SetBearerToken(token); //proceed with auth testing
This allows the AccessTokenValidation middleware in your Api project to communicate directly with your In-Memory IdentityServer without the need to jump through hoops.
As a side note, for an Api project, I find it useful to add IdentityServerAuthenticationOptions to the services collection in Startup.cs using TryAddSingleton instead of creating it inline:
public void ConfigureServices(IServiceCollection services) { services.TryAddSingleton(new IdentityServerAuthenticationOptions { Authority = Configuration.IdentityServerAuthority(), ScopeName = "api1", ScopeSecret = "secret", //..., }); } public void Configure(IApplicationBuilder app) { var options = app.ApplicationServices.GetService<IdentityServerAuthenticationOptions>() app.UseIdentityServerAuthentication(options); //... }
This allows you to register the IdentityServerAuthenticationOptions object in your tests without having to alter the code in the Api project.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With