Создание интеграционного теста для API AspNetCore, использующего IdentityServer 4 для проверки подлинности

Я создал простой AspNetCore 2.2 API, который использует IdentityServer 4 для обработки OAuth. Он работает нормально, но теперь я хотел бы добавить интеграционные тесты и недавно обнаружил это. Я использовал его для создания нескольких тестов, и все они работали нормально, пока у меня не было атрибута [Authorize] на моих контроллерах, но очевидно, что этот атрибут должен быть там.

Я наткнулся на этот вопрос о стеке и из ответов, данных там, я попытался собрать тест, но я все еще получаю ответ Unauthorized, когда пытаюсь запустить тесты.

Пожалуйста, обрати внимание: Я действительно не знаю, какие детали мне следует использовать при создании клиента.

  • Какими должны быть разрешенные области? (Должны ли они соответствовать реальным области)

Также при создании IdentityServerWebHostBuilder

  • На что мне перейти .AddApiResources? (Может глупый вопрос, но это имеет значение)

Если кто-нибудь может направить меня, это было бы очень признательно.

Вот мой тест:

[Fact]
public async Task Attempt_To_Test_InMemory_IdentityServer()
{
    // Create a client
        var clientConfiguration = new ClientConfiguration("MyClient", "MySecret");

        var client = new Client
        {
            ClientId = clientConfiguration.Id,
            ClientSecrets = new List<Secret>
            {
                new Secret(clientConfiguration.Secret.Sha256())
            },
            AllowedScopes = new[] { "api1" },
            AllowedGrantTypes = new[] { GrantType.ClientCredentials },
            AccessTokenType = AccessTokenType.Jwt,
            AllowOfflineAccess = true
        };

        var webHostBuilder = new IdentityServerWebHostBuilder()
            .AddClients(client)
            .AddApiResources(new ApiResource("api1", "api1name"))
            .CreateWebHostBuilder();

        var identityServerProxy = new IdentityServerProxy(webHostBuilder);
        var tokenResponse = await identityServerProxy.GetClientAccessTokenAsync(clientConfiguration, "api1");

        // *****
        // Note: creating an IdentityServerProxy above in order to get an access token
        // causes the next line to throw an exception stating: WebHostBuilder allows creation only of a single instance of WebHost
        // *****

        // Create an auth server from the IdentityServerWebHostBuilder 
        HttpMessageHandler handler;
        try
        {
            var fakeAuthServer = new TestServer(webHostBuilder);
            handler = fakeAuthServer.CreateHandler();
        }
        catch (Exception e)
        {
            throw;
        }

        // Create an auth server from the IdentityServerWebHostBuilder 
        HttpMessageHandler handler;
        try
        {
            var fakeAuthServer = new TestServer(webHostBuilder);
            handler = fakeAuthServer.CreateHandler();
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            throw;
        }

        // Set the BackChannelHandler of the 'production' IdentityServer to use the 
        // handler form the fakeAuthServer
        Startup.BackChannelHandler = handler;
        // Create the apiServer
        var apiServer = new TestServer(new WebHostBuilder().UseStartup<Startup>());
        var apiClient = apiServer.CreateClient();


        apiClient.SetBearerToken(tokenResponse.AccessToken);

        var user = new User
        {
            Username = "[email protected]",
            Password = "Password-123"
        };

        var req = new HttpRequestMessage(new HttpMethod("GET"), "/api/users/login")
        {
            Content = new StringContent(JsonConvert.SerializeObject(user), Encoding.UTF8, "application/json"),
        };

        // Act
        var response = await apiClient.SendAsync(req);

        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);

}

Мой класс запуска:

public class Startup
{

    public IConfiguration Configuration { get; }
    public static HttpMessageHandler BackChannelHandler { get; set; }

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        ConfigureAuth(services);    
        services.AddTransient<IPassportService, PassportService>();
        services.Configure<ApiBehaviorOptions>(options =>
        {
            options.SuppressModelStateInvalidFilter = true;
        });

    }

    protected virtual void ConfigureAuth(IServiceCollection services)
    {
        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.Authority = Configuration.GetValue<string>("IdentityServerAuthority");
                options.Audience = Configuration.GetValue<string>("IdentityServerAudience");
                options.BackchannelHttpHandler = BackChannelHandler;
            });
    }


    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseHsts();
        }

        app.UseAuthentication();
        app.UseHttpsRedirection();
        app.UseMvc();
        app.UseExceptionMiddleware();
    }
}

Не могли бы вы добавить код, используемый для фактического запроса токена? Какую ошибку вы получаете?

alsami 09.04.2019 13:03

@alsami Я получаю «неавторизованный», что имеет смысл, потому что я не передавал токен носителя, поэтому я добавил некоторый код, который, как я думал, сделает это, но теперь вызывает другие проблемы, которые, надеюсь, я объяснил в комментариях в обновленном коде.

Simon Lomax 09.04.2019 13:27

@alsami Хотя теперь я могу получить токен доступа, я не знаю, как связать свой TestServer для API с IdentityServerProxy

Simon Lomax 09.04.2019 16:52

Можете ли вы предоставить полный исходный код на github? Мне нужно будет проверить вручную, чтобы увидеть, что не работает.

alsami 09.04.2019 16:56
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
4
1 846
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Редактировать:

Предложение ниже было одной проблемой. Исходный исходный код не удался из-за исключения при попытке сборки WebHostBuilderдважды. Во-вторых, файл конфигурации присутствовал только в проекте API, а не в тестовом проекте, поэтому полномочия также не были установлены.

Вместо того, чтобы делать это

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
   .AddJwtBearer(options =>
   {
       options.Authority = Configuration.GetValue<string>("IdentityServerAuthority");
       options.Audience = Configuration.GetValue<string>("IdentityServerAudience");
       options.BackchannelHttpHandler = BackChannelHandler;
   });

Вы должны сделать что-то вроде этого:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
   .AddIdentityServerAuthentication(options =>
   {
      options.Authority = Configuration.GetValue<string>("IdentityServerAuthority");
      options.JwtBackChannelHandler = BackChannelHandler;
    });

Вы можете найти образец здесь.

Надеюсь, это поможет, сработало для меня!

Спасибо за ваше предложение, но я тоже не мог заставить это работать. Я добавил git репозиторий, как вы просили. Это очень простой API, который использует IdentityServer для аутентификации. В проекте также есть проект IntegrationTest, который включает тест, который я разместил здесь. Я был бы очень признателен, если бы вы могли взглянуть и сказать мне, где я ошибаюсь.

Simon Lomax 10.04.2019 11:24

получил мерж-реквест, который это исправил: github.com/simax/SuperSimpleAPI/pull/1

alsami 10.04.2019 12:42

Это потрясающе. Большое вам спасибо за вашу помощь. Одна вещь - среди многих :), которая меня смущала, заключалась в том, что я не понимал, что мне нужно вызвать CreateHandler() из identityServerProxy.IdentityServer, я думаю, что искал это прямо у identityServerProxy. Еще раз большое спасибо за создание пакета nuget и за вашу помощь — это очень ценно.

Simon Lomax 10.04.2019 13:12

@alsami Я попробовал ваше исправление, github.com/simax/SuperSimpleAPI, и я продолжаю получать http 302. Я подал заявку, как вы описали. Есть идеи, почему?

Ktt 22.01.2020 11:15

@Ktt, вы могли бы предоставить репо, чтобы я мог его воспроизвести. У меня в голове нет ответа, почему это происходит.

alsami 22.01.2020 13:06

@alsami Я преодолел это, наша инициализация IOC намного сложнее, чем предоставленный вами код, в конечном итоге попытка за попыткой, мне пришлось удалить строку app.UseIdentityServer() из TestStartup и использовать IdentityServerProxy и его обработчик, как в вашем примере. Думаю написать об этом в БЖ. Я поделюсь с вами ссылкой, когда сделаю. Ваш пример очень помог. Спасибо большое

Ktt 22.01.2020 14:56

@Ktt рад, что все получилось! Да, я тоже думал об этом. Еще один образец можно найти здесь, между прочим: github.com/cleancodelabs/…

alsami 22.01.2020 14:59

@alsami, вот что я опубликовал, я также сослался на ваш код в github. medium.com/@kutlu_eren/…

Ktt 30.01.2020 08:37

Спасибо! Поделюсь!

alsami 30.01.2020 09:30

Возможно, вы также захотите добавить ссылку на пакет :) nuget.org/packages/IdentityServer4.Contrib.AspNetCore.Testin‌​g

alsami 30.01.2020 09:35

@alsami, обязательно сойдет, еще раз спасибо :) пожалуйста, найдите ссылку в блоге, где она указана как «Возможно, вы захотите проверить ее потрясающие реализации здесь».

Ktt 30.01.2020 13:54

Если вы не хотите полагаться на статическую переменную для хранения HttpHandler, я обнаружил, что работает следующее. Я думаю, что это намного чище.

Сначала создайте объект, который вы можете создать до того, как будет создан ваш 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;

Решение, которое не влияет на производственный код:

public class TestApiWebApplicationFactory<TStartup>
    : WebApplicationFactory<TStartup> where TStartup : class
{
    private readonly HttpClient _identityServerClient;

    public TestApiWebApplicationFactory(HttpClient identityServerClient)
    {
        _identityServerClient = identityServerClient;
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        base.ConfigureWebHost(builder);

        builder.ConfigureServices(
            s =>
            {
                s.AddSingleton<IConfigureOptions<JwtBearerOptions>>(services =>
                {
                    return new TestJwtBearerOptions(_identityServerClient);
                });
            });
    }
}

и его использование:

 _factory = new WebApplicationFactory<Startup>()
        {
            ClientOptions = {BaseAddress = new Uri("http://localhost:5000/")}
        };

        _apiFactory = new TestApiWebApplicationFactory<SampleApi.Startup>(_factory.CreateClient())
        {
            ClientOptions = {BaseAddress = new Uri("http://localhost:5001/")}
        };

TestJwtBearerOptions просто проксирует запросы к identityServerClient. Реализацию вы можете найти здесь: https://gist.github.com/ru-sh/048e155d73263912297f1de1539a2687

Ссылка не работает: у вас есть код для TestJwtBearerOptions?

aksu 13.01.2022 07:00

Нет, случайно удалил. Но это должно быть довольно просто реализовать. Вам нужно будет наследовать JwtBearerOptions и поместить точки останова в его методы, чтобы выяснить, какой из них обрабатывает HTTP-сообщения. AFAIR, это был BackchannelHttpHandler.

Shakirov Ruslan 14.01.2022 08:52

Другие вопросы по теме