Модульное тестирование AspNet.Security.OpenIdConnect.Server (OIDC/ASOS)

Я использую Visual Studio 2015 Enterprise Update 1 и ASP.NET 5 rc1-final для создания конечной точки, которая выдает и использует токены JWT, как подробно описано здесь.

Теперь я перехожу к модульному тестированию и сталкиваюсь с трудностями при тестировании некоторых аспектов AspNet.Security.OpenIdConnect.Server (OIDC/ASOS). В частности, некоторые примитивы ASOS, такие как LogoutEndpointContext, не являются абстрактными, поэтому их нелегко подделать. Обычно я просто добавляю fake к таким типам, но Похоже, что DNX не поддерживает подделки, по крайней мере, пока. Эти типы также запечатаны, поэтому я не могу их специализировать.

Это вынудило меня написать некий ненадежный код отражения, чтобы создать такие запечатанные типы ASOS. Вот пример тестов XUnit, для которого требуется LogoutEndpointContext, чтобы я мог проверить свою обработку событий OpenIdConnectServerProvider (в этом случае выход из системы без POST должен вызвать исключение); обратите внимание на отражение, которое мне нужно сделать, чтобы создать экземпляр LogoutEndpointContext:

[Fact]
async public Task API_Initialization_Services_AuthenticatedUser_Authentication_LogoutEndpoint_XSRF_Unit()
{
    // Arrange

        Mock<HttpRequest> mockRequest = new Mock<HttpRequest>();
        mockRequest.SetupGet(a => a.Method).Returns(() => "Not Post");
        Mock<HttpContext> mockContext = new Mock<HttpContext>();
        mockContext.SetupGet(a => a.Request).Returns(() => mockRequest.Object);

        OpenIdConnectServerOptions options = new OpenIdConnectServerOptions();

        OpenIdConnectMessage request = new OpenIdConnectMessage();

        // I would prefer not to use reflection
        var ctorInfo = typeof(LogoutEndpointContext).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance).Single();

        LogoutEndpointContext context = (LogoutEndpointContext)ctorInfo.Invoke(new object[] {mockContext.Object,options, request});

    // Act

        AuthenticationEvents authenticationEvents = new AuthenticationEvents();

    // Assert

        await Assert.ThrowsAsync<SecurityTokenValidationException>(() => authenticationEvents.LogoutEndpoint(context));
}

Будем очень признательны за любые советы о том, как лучше создавать/имитировать/подделывать/специализировать запечатанные типы ASOS, такие как LogoutEndpointContext.


person 42vogons    schedule 04.01.2016    source источник
comment
Во-первых, вы не должны тестировать сам код фреймворка, но вам нужно тестировать код, использующий код фреймворка. Правильный способ сделать это — обернуть объект каким-то фиктивным интерфейсом, который вы затем перенаправляете на содержащийся в нем объект. Примером этого является HttpContextBase/HttpContextWrapper. К сожалению, вы не можете передать эти макеты другим функциям фреймворка, что делает этот подход менее полезным в некоторых ситуациях или, по крайней мере, требует немного больше работы.   -  person Erik Funkenbusch    schedule 04.01.2016


Ответы (1)


При тестировании промежуточного ПО, которое сильно зависит от контекста HTTP, модульное тестирование редко бывает самым простым вариантом. Наш рекомендуемый подход здесь — выбрать функциональное тестирование, используя TestServer для настройки конвейера в памяти:

public class OAuthLogoutEndpointTests {
    [Fact]
    public async Task EnsureThatOnlyPostLogoutRequestsAreValid() {
        // Arrange
        var server = CreateAuthorizationServer();
        var client = server.CreateClient();

        var request = new HttpRequestMessage(HttpMethod.Get, "/connect/logout");

        // Act
        var response = await client.SendAsync(request);

        // Assert
        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
    }

    private static TestServer CreateAuthorizationServer(Action<OpenIdConnectServerOptions> configuration = null) {
        var builder = new WebHostBuilder();

        builder.UseEnvironment("Testing");

        builder.ConfigureServices(services => {
            services.AddAuthentication();
            services.AddCaching();
            services.AddLogging();
        });

        builder.Configure(app => {
            // Add a new OpenID Connect server instance.
            app.UseOpenIdConnectServer(options => {
                // Use your own provider:
                // options.Provider = new AuthenticationEvents();

                options.Provider = new OpenIdConnectServerProvider {
                    OnValidateLogoutRequest = context => {
                        // Reject non-POST logout requests.
                        if (!string.Equals(context.HttpContext.Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) {
                            context.Reject(
                                error: OpenIdConnectConstants.Errors.InvalidRequest,
                                description: "Only POST requests are supported.");
                        }

                        return Task.FromResult(0);
                    }
                };

                // Run the configuration delegate
                // registered by the unit tests.
                configuration?.Invoke(options);
            });
        });

        return new TestServer(builder);
    }
}

(вы можете найти больше тестов здесь: https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions/blob/master/test/AspNet.Security.OAuth.Introspection.Tests/OAuthIntrospectionMiddlewareTests.cs)

Редактировать: остальная часть этого ответа больше не применяется, так как классы контекста были распечатаны в бета-версии ASOS 5.


В ASOS все контекстные классы запечатаны, а их конструктор помечен как внутренний, чтобы разработчики не могли создавать их экземпляры и вручную вызывать провайдера в производственном коде. Тем не менее, если вы твердо уверены, что нам не следует этого делать, смело открывайте новый тикет в репозитории GitHub.

Последнее замечание: отклонение запросов GET, вероятно, не очень хорошая идея, если вы хотите, чтобы ваш сервер OIDC соответствовал спецификациям, поскольку это единственный глагол, явно упомянутый в спецификациях.

person Kévin Chalet    schedule 04.01.2016
comment
Спасибо, @Pinpoint — я добавил билет здесь для вашего сведения. - person 42vogons; 05.01.2016
comment
Ответил. Не стесняйтесь отправить PR ;) - person Kévin Chalet; 07.01.2016