Тестирование интеграции с использованием памяти 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;