Тестирование интеграции с использованием памяти IdentityServer
У меня есть API, который использует IdentityServer4 для проверки маркера.
Я хочу unit test использовать этот API с помощью TestServer в памяти. Я хотел бы разместить IdentityServer в тестовом сервере в памяти.
Мне удалось создать токен из IdentityServer.
Вот как далеко я пришел, но получаю сообщение об ошибке "Не удалось получить конфигурацию из http://localhost:54100/.well-known/openid-configuration"
Api использует [Авторизовать] -атрибут с разными политиками. Это то, что я хочу проверить.
Можно ли это сделать, и что я делаю неправильно?
Я попытался взглянуть на исходный код для IdentityServer4, но не столкнулся с аналогичным сценарием интеграции.
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();
}
И в моем модульном тесте:
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;
}
Ответы
Ответ 1
Я думаю, вам, вероятно, нужно сделать двойную подделку для вашего промежуточного ПО авторизации в зависимости от того, сколько функций вы хотите. Таким образом, в основном вы хотите, чтобы промежуточное программное обеспечение делало все, что промежуточное ПО авторизации делает минус обратный вызов канала в документ обнаружения.
IdentityServer4.AccessTokenValidation - обертка вокруг двух посредников. Средство JwtBearerAuthentication
и промежуточное программное обеспечение OAuth2IntrospectionAuthentication
. Оба они захватывают документ обнаружения через http, чтобы использовать для проверки токена. Это проблема, если вы хотите выполнить автономный тест в памяти.
Если вы хотите решить проблему, вам, вероятно, понадобится сделать поддельную версию app.UseIdentityServerAuthentication
, которая не выполняет внешний вызов, который извлекает документ обнаружения. Он заполняет только HttpContext, чтобы ваши политики [Authorize] могли быть протестированы.
Посмотрите, как мясо IdentityServer4.AccessTokenValidation выглядит здесь. Посмотрите, как выглядит промежуточное ПО JwtBearer здесь
Ответ 2
Вы были на правильном пути с кодом, опубликованным в вашем первоначальном вопросе.
Объект IdentityServerAuthenticationOptions имеет свойства для переопределения стандартного HttpMessageHandlers, который он использует для связи по обратному каналу.
После объединения этого метода с CreateHandler() на TestServer вы получите:
//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
Это позволяет промежуточному программному обеспечению AccessTokenValidation в вашем проекте Api напрямую взаимодействовать с вашим встроенным IdentityServer без необходимости переходить через обручи.
В качестве побочного примечания для проекта Api я считаю полезным добавить IdentityServerAuthenticationOptions в набор сервисов в Startup.cs, используя TryAddSingleton. > вместо его создания:
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);
//...
}
Это позволяет вам зарегистрировать объект IdentityServerAuthenticationOptions в ваших тестах без изменения кода в проекте Api.
Ответ 3
Я понимаю, что нужен более полный ответ, чем то, что написал @james-fera. Я учился на его ответе и создал проект github, состоящий из тестового проекта и проекта API. Код должен быть понятен и не сложен для понимания.
https://github.com/emedbo/identityserver-test-template
Класс IdentityServerSetup.cs
https://github.com/emedbo/identityserver-test-template/blob/master/tests/API.Tests/Config/IdentityServerSetup.cs можно абстрагировать, например, NuGetted, оставляя базовый класс IntegrationTestBase.cs
Суть в том, что тестовый IdentityServer может работать так же, как обычный IdentityServer с пользователями, клиентами, областями, паролями и т.д. Я сделал метод DELETE [Authorize (Role = "admin)], чтобы доказать это.
Вместо того, чтобы публиковать здесь код, я рекомендую прочитать @james-fera post, чтобы получить основы, затем потянуть мой проект и запустить тесты.
IdentityServer - это отличный инструмент, и благодаря возможности использовать среду TestServer он становится еще лучше.
Ответ 4
Тестовый запуск API:
public class Startup
{
public static HttpMessageHandler BackChannelHandler { get; set; }
public void Configuration(IAppBuilder app)
{
//accept access tokens from identityserver and require a scope of 'Test'
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost",
BackchannelHttpHandler = BackChannelHandler,
...
});
...
}
}
Назначение AuthServer.Handler для TestApi BackChannelHandler в моем проекте модульного тестирования:
protected TestServer AuthServer { get; set; }
protected TestServer MockApiServer { get; set; }
protected TestServer TestApiServer { get; set; }
[OneTimeSetUp]
public void Setup()
{
...
AuthServer = TestServer.Create<AuthenticationServer.Startup>();
TestApi.Startup.BackChannelHandler = AuthServer.CreateHandler();
TestApiServer = TestServer.Create<TestApi.Startup>();
}
Ответ 5
Хитрость заключается в том, чтобы создать обработчик с помощью TestServer
который настроен на использование IdentityServer4
. Образцы можно найти здесь.
Для этого я создал пакет nuget, доступный для установки и тестирования, используя библиотеку Microsoft.AspNetCore.Mvc.Testing и последнюю версию IdentityServer4
.
Он инкапсулирует весь код инфраструктуры, необходимый для создания соответствующего WebHostBuilder
который затем используется для создания TestServer
путем генерации HttpMessageHandler
для HttpClient
используемого внутри.
Ответ 6
Ни один из других ответов не сработал для меня, потому что они полагаются на 1) статическое поле для хранения вашего HttpHandler и 2) класс Startup, чтобы знать, что ему может быть предоставлен обработчик теста. Я нашел следующее, чтобы работать, что я думаю, намного чище.
Сначала создайте объект, который вы можете создать до создания TestHost. Это потому, что у вас не будет HttpHandler до тех пор, пока не будет создан TestHost, поэтому вам нужно использовать оболочку.
public class TestHttpMessageHandler : DelegatingHandler
{
private ILogger _logger;
public TestHttpMessageHandler(ILogger logger)
{
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
_logger.Information($"Sending HTTP message using TestHttpMessageHandler. Uri: '{request.RequestUri.ToString()}'");
if (WrappedMessageHandler == null) throw new Exception("You must set WrappedMessageHandler before TestHttpMessageHandler can be used.");
var method = typeof(HttpMessageHandler).GetMethod("SendAsync", BindingFlags.Instance | BindingFlags.NonPublic);
var result = method.Invoke(this.WrappedMessageHandler, new object[] { request, cancellationToken });
return await (Task<HttpResponseMessage>)result;
}
public HttpMessageHandler WrappedMessageHandler { get; set; }
}
Тогда
var testMessageHandler = new TestHttpMessageHandler(logger);
var webHostBuilder = new WebHostBuilder()
...
services.PostConfigureAll<JwtBearerOptions>(options =>
{
options.Audience = "http://localhost";
options.Authority = "http://localhost";
options.BackchannelHttpHandler = testMessageHandler;
});
...
var server = new TestServer(webHostBuilder);
var innerHttpMessageHandler = server.CreateHandler();
testMessageHandler.WrappedMessageHandler = innerHttpMessageHandler;