Поиск  
Always will be ready notify the world about expectations as easy as possible: job change page
Jun 1

ASP.NET 8 Token Authentication for Web API and React with Integration Testing (Part 2: Integration Test)

Автор:
Источник:
Просмотров:
3968

In the second part of our series, the focus shifts towards validating the security and reliability of our ASP.NET 8 Web API through comprehensive integration testing. Integration testing plays a critical role in ensuring that our authentication mechanisms work as intended, under various scenarios and edge cases. We’ll guide you through setting up your testing environment, writing test cases that cover a wide range of authentication scenarios, and interpreting the results to refine your authentication system. This article is designed to provide you with a deep understanding of how to effectively test web APIs, ensuring that your application remains secure and functions correctly under all circumstances.

Part 1: Setting up API
Part 2: Setting up integration tests for API project
Part 3: Setting up React client

ApiTest project: Integration test project for API

The ApiTest project is configured with xUnit, Microsoft.AspNetCore.Mvc.Testing, and FluentAssertions, are here to ensure our API runs smoothly.

Integration tests
Integration tests

ApiTest project classes

CustomWebApplicationFactory, the purpose of setting up a custom environment for integration testing. It’s using an in-memory database to isolate the tests, and it’s removing and replacing services appropriately.

public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.UseEnvironment("Test");
        builder.ConfigureServices(services =>
        {
            var context = services.FirstOrDefault(descriptor => descriptor.ServiceType == typeof(ApplicationDbContext));
            if (context != null)
            {
                services.Remove(context);
                var options = services.Where(r => r.ServiceType == typeof(DbContextOptions)
                  || r.ServiceType.IsGenericType && r.ServiceType.GetGenericTypeDefinition() == typeof(DbContextOptions<>)).ToArray();
                foreach (var option in options)
                {
                    services.Remove(option);
                }
            }

            // Add a new registration for ApplicationDbContext with an in-memory database
            services.AddDbContext<ApplicationDbContext>(options =>
            {
                // Provide a unique name for your in-memory database
                options.UseInMemoryDatabase("InMemoryDatabaseNameX");
            });
        });
    }
}

Settings up the environment as “Test”

builder.UseEnvironment("Test");

It’s going to load appsettings.test.json

{
  "TokenSettings": {
    "Issuer": "TestApi",
    "Audience": "TestApp",
    "SecretKey": "DFDGERsjsfjepoeoe@@#$$@$@123112sdaaadasQEWw",
    "TokenExpireSeconds": 10,
    "RefreshTokenExpireSeconds": 20
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

This code block is preparing the environment for integration tests by ensuring a clean setup for the ApplicationDbContext service, removing any existing registrations, and then configuring it to use an in-memory database with a unique name.

var context = services.FirstOrDefault(descriptor => descriptor.ServiceType == typeof(ApplicationDbContext));
if (context != null)
{
    services.Remove(context);
    var options = services.Where(r => r.ServiceType == typeof(DbContextOptions)
      || r.ServiceType.IsGenericType && r.ServiceType.GetGenericTypeDefinition() == typeof(DbContextOptions<>)).ToArray();
    foreach (var option in options)
    {
        services.Remove(option);
    }
}

// Add a new registration for ApplicationDbContext with an in-memory database
services.AddDbContext<ApplicationDbContext>(options =>
{
    // Provide a unique name for your in-memory database
    options.UseInMemoryDatabase("InMemoryDatabaseNameX");
});

Perfect! With the groundwork laid, let’s dive into the actual testing phase.

UserEndPointTest covers different scenarios related to user authentication and registration, checking the API’s behavior under various conditions.

public class UserEndPointTest(CustomWebApplicationFactory<Program> factory) : IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client = factory.CreateClient();

    [Fact]
    public async void LoginSuccessTest()
    {
        var userLoginRequest = new UserLoginRequest
        {
            Email = "UnifiedAppAdmin",
            Password = "UnifiedAppAdmin1!"
        };

        var jsonContent = JsonSerializer.Serialize(userLoginRequest);
        var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
        var response = await _client.PostAsync("/User/Login", content);
        if (response.IsSuccessStatusCode)
        {
            response.Should().NotBeNull();
            var userLoginResponse = await response.Content.ReadFromJsonAsync<AppResponse<UserLoginResponse>>();
            userLoginResponse.Should().NotBeNull();
            userLoginResponse?.IsSucceed.Should().BeTrue();
            userLoginResponse?.Data.Should().NotBeNull();
            userLoginResponse?.Data?.AccessToken.Should().NotBeNull();
            userLoginResponse?.Data?.RefreshToken.Should().NotBeNull();
        }
        else
        {
            Assert.Fail("Api call failed.");
        }
    }

    [Fact]
    public async void LoginFailTest()
    {
        var userLoginRequest = new UserLoginRequest
        {
            Email = "UnifiedAppAdmin",
            Password = "wrong_password"
        };

        var jsonContent = JsonSerializer.Serialize(userLoginRequest);
        var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
        var response = await _client.PostAsync("/User/Login", content);
        if (response.IsSuccessStatusCode)
        {
            response.Should().NotBeNull();
            var userLoginResponse = await response.Content.ReadFromJsonAsync<AppResponse<UserLoginResponse>>();
            userLoginResponse.Should().NotBeNull();
            userLoginResponse?.IsSucceed.Should().BeFalse();
            userLoginResponse?.Data.Should().BeNull();
        }
        else
        {
            Assert.Fail("Api call failed.");
        }
    }

    [Fact]
    public async void RegistrationTest()
    {
        var userLoginRequest = new UserRegisterRequest
        {
            Email = "UnifiedAppAdmin1",
            Password = "Pass@#123"
        };

        var jsonContent = JsonSerializer.Serialize(userLoginRequest);
        var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
        var response = await _client.PostAsync("/User/Register", content);
        if (response.IsSuccessStatusCode)
        {
            response.Should().NotBeNull();
            var userRegisterResponse = await response.Content.ReadFromJsonAsync<AppResponse<bool>>();
            userRegisterResponse.Should().NotBeNull();
            userRegisterResponse?.IsSucceed.Should().BeTrue();
            userRegisterResponse?.Data.Should().BeTrue();
        }
        else
        {
            Assert.Fail("Api call failed.");
        }
    }

    [Fact]
    public async void RegistrationExistingUserTest()
    {
        var userLoginRequest = new UserRegisterRequest
        {
            Email = "UnifiedAppAdmin",
            Password = "Pass@#123"
        };

        var jsonContent = JsonSerializer.Serialize(userLoginRequest);
        var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
        var response = await _client.PostAsync("/User/Register", content);
        if (response.IsSuccessStatusCode)
        {
            response.Should().NotBeNull();
            var userRegisterResponse = await response.Content.ReadFromJsonAsync<AppResponse<bool>>();
            userRegisterResponse.Should().NotBeNull();
            userRegisterResponse?.IsSucceed.Should().BeFalse();
            userRegisterResponse?.Data.Should().BeFalse();
            userRegisterResponse?.Messages.Any(m => m.Key == "DuplicateUserName").Should().BeTrue();
        }
        else
        {
            Assert.Fail("Api call failed.");
        }
    }

    [Fact]
    public async void RegistrationPasswordTest()
    {
        var userLoginRequest = new UserRegisterRequest
        {
            Email = "UnifiedAppAdmin",
            Password = "123"
        };

        var jsonContent = JsonSerializer.Serialize(userLoginRequest);
        var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
        var response = await _client.PostAsync("/User/Register", content);
        if (response.IsSuccessStatusCode)
        {
            response.Should().NotBeNull();
            var userRegisterResponse = await response.Content.ReadFromJsonAsync<AppResponse<bool>>();
            userRegisterResponse.Should().NotBeNull();
            userRegisterResponse?.IsSucceed.Should().BeFalse();
            userRegisterResponse?.Data.Should().BeFalse();
            userRegisterResponse?.Messages.Any(m => m.Key == "PasswordRequiresLower").Should().BeTrue();
            userRegisterResponse?.Messages.Any(m => m.Key == "PasswordTooShort").Should().BeTrue();
            userRegisterResponse?.Messages.Any(m => m.Key == "PasswordRequiresNonAlphanumeric").Should().BeTrue();
            userRegisterResponse?.Messages.Any(m => m.Key == "PasswordRequiresUpper").Should().BeTrue();
        }
        else
        {
            Assert.Fail("Api call failed.");
        }
    }
}

1. LoginSuccessTest:

  • Scenario: Attempts to log in with correct credentials.
  • Expected outcome: Expect a successful response with a valid access token and refresh token.

2. LoginFailTest:

  • Scenario: Attempts to log in with incorrect credentials.
  • Expected outcome: Expect an unsuccessful response with no access token and a failed login status.

3. RegistrationTest:

  • Scenario: Attempts to register a new user.
  • Expected outcome: Expect a successful response indicating a successful registration.

4. RegistrationExistingUserTest:

  • Scenario: Attempts to register an existing user.
  • Expected outcome: Expect an unsuccessful response indicating a failed registration due to an existing user.

5. RegistrationPasswordTest:

  • Scenario: Attempts to register a user with a weak password.
  • Expected outcome: Expect an unsuccessful response with specific error messages indicating password strength requirements.

TokenTest class contains a set of tests that cover different scenarios related to token-based authentication and refresh token functionality.

public class TokenTest(CustomWebApplicationFactory<Program> factory) : IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client = factory.CreateClient();

    [Fact]
    public async void ApiSuccessTest()
    {
        var token = await GetToken();
        var apiRes = await GetSecureApiRes(token.AccessToken);
        apiRes.Should().NotBeNull();
        apiRes.StatusCode.Should().Be(HttpStatusCode.OK);

    }

    [Fact]
    public async void ApiFailureTest()
    {
        var apiRes = await GetSecureApiRes("");
        apiRes.Should().NotBeNull();
        apiRes.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
    }

    [Fact]
    public async void TokenExpireTest()
    {
        var token = await GetToken();

        await Task.Delay(12000);
        HttpRequestMessage request = new(HttpMethod.Post, "/User/Profile");
        request.Headers.Add("Authorization", "Bearer " + token.AccessToken);
        var apiRes = await _client.SendAsync(request);
        apiRes.Should().NotBeNull();
        apiRes.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
    }

    [Fact]
    public async void GetRefreshTokenTest()
    {
        var token = await GetToken();
        await Task.Delay(12000);
        var rtRes = await GetRefreshToken(new UserRefreshTokenRequest
        {
            AccessToken = token.AccessToken,
            RefreshToken = token.RefreshToken
        });
        rtRes.Should().NotBeNull();
        rtRes.StatusCode.Should().Be(HttpStatusCode.OK);
        var userRefreshTokenResponse = await rtRes.Content.ReadFromJsonAsync<AppResponse<UserRefreshTokenResponse>>();
        userRefreshTokenResponse.Should().NotBeNull();
        userRefreshTokenResponse?.IsSucceed.Should().BeTrue();
        userRefreshTokenResponse?.Data?.Should().NotBeNull();
        var apiRes = await GetSecureApiRes(userRefreshTokenResponse?.Data?.AccessToken ?? "");
        apiRes.Should().NotBeNull();
        apiRes.StatusCode.Should().Be(HttpStatusCode.OK);
    }

    [Fact]
    public async void RefreshTokenExpireTest()
    {
        var token = await GetToken();
        await Task.Delay(22000);
        var rtRes = await GetRefreshToken(new UserRefreshTokenRequest
        {
            AccessToken = token.AccessToken,
            RefreshToken = token.RefreshToken
        });
        rtRes.Should().NotBeNull();
        rtRes.StatusCode.Should().Be(HttpStatusCode.OK);
        var userRefreshTokenResponse = await rtRes.Content.ReadFromJsonAsync<AppResponse<UserRefreshTokenResponse>>();
        userRefreshTokenResponse.Should().NotBeNull();
        userRefreshTokenResponse?.IsSucceed.Should().BeFalse();
        userRefreshTokenResponse?.Data?.Should().BeNull();
        userRefreshTokenResponse?.Messages.Any(m => m.Key == "token").Should().BeTrue();
    }

    private async Task<UserLoginResponse> GetToken()
    {
        var userLoginRequest = new UserLoginRequest
        {
            Email = "UnifiedAppAdmin",
            Password = "UnifiedAppAdmin1!"
        };

        var jsonContent = JsonSerializer.Serialize(userLoginRequest);
        var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
        var response = await _client.PostAsync("/User/Login", content);
        var userLoginResponse = await response.Content.ReadFromJsonAsync<AppResponse<UserLoginResponse>>();

        return userLoginResponse?.Data ?? default!;
    }

    private async Task<HttpResponseMessage> GetSecureApiRes(string accessToken)
    {
        HttpRequestMessage request = new(HttpMethod.Post, "/User/Profile");
        request.Headers.Add("Authorization", "Bearer " + accessToken);

        return await _client.SendAsync(request);
    }

    private async Task<HttpResponseMessage> GetRefreshToken(UserRefreshTokenRequest userRefreshTokenRequest)
    {

        var jsonContent = JsonSerializer.Serialize(userRefreshTokenRequest);
        var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");

        return await _client.PostAsync("/User/RefreshToken", content);
    }
}

1. ApiSuccessTest:

  • Scenario: Requests a secure API resource with a valid access token.
  • Expected outcome: Expect a successful response with an HTTP status code of OK.

2. ApiFailureTest:

  • Scenario: Requests a secure API resource with an empty access token.
  • Expected outcome: Expect an unauthorized response with an HTTP status code indicating failure.

3. TokenExpireTest:

  • Scenario: Attempts to use an expired access token to access a secure API resource.
  • Expected outcome: Expect an unauthorized response due to the expired token.

4. GetRefreshTokenTest:

  • Scenario: Obtains a new access token using a valid refresh token.
  • Expected outcome: Expect a successful response with a new access token, allowing access to a secure API resource.

5. RefreshTokenExpireTest:

  • Scenario: Attempts to refresh the token with an expired refresh token.
  • Expected outcome: Expect a failure response, indicating that the refresh token has expired.

Summary

This article serves as a detailed guide on establishing integration tests for a .NET 8 Web API, with a specific focus on token authentication. It provides practical examples and scenarios to validate the effectiveness and reliability of the authentication system.

Source Code

For the complete code and more examples, check out the GitHub repository: UnifiedApp

Похожее
Jul 29
Author: C# Programming Obinna “Anderson” Asiegbulam
Introduction Open Data Protocol (OData) is a standard protocol for building and consuming RESTful APIs. It extends traditional REST-based data access techniques by providing a uniform way to expose, structure, and manipulate data. Originally developed by Microsoft, OData is widely...
Sep 11, 2023
Author: Artem A. Semenov
In today’s rapidly evolving web development landscape, selecting the right framework is crucial for delivering efficient, scalable, and maintainable applications. React and Blazor are two leading frameworks that have gained significant attention in recent years. As developers and businesses weigh...
Aug 8
Author: Anton Martyniuk
Integration testing is a type of software testing essential for validating the interactions between different components of an application, ensuring they work together as expected. The main goal of integration testing is to identify any issues that may arise when...
Mar 12, 2021
Author: Bruno Joaquim
Exceptions are part of any software and can happen any time even where we are absolutely sure that nothing can go wrong. Exceptions are inevitable, we all agree with that. We should always deal with them in our applications to...
Написать сообщение
Тип
Почта
Имя
*Сообщение
RSS
Если вам понравился этот сайт и вы хотите меня поддержать, вы можете
Soft skills: 18 самых важных навыков, которыми должен владеть каждый работник
Какого черта мы нанимаем, или осмысленность собеседований в IT
Нагрузочное тестирование: что? где? когда?
Performance review, ачивки и погоня за повышением грейда — что может причинить боль сотруднику IT-компании?
Дюжина логических задач с собеседований
Четыре типажа программистов
GraphQL решает кучу проблем — рассказываем, за что мы его любим
Разбираемся с middleware в ASP.NET Core
Как избавиться от прокрастинации до того, как она разрушит вашу карьеру
Функции и хранимые процедуры в PostgreSQL: зачем нужны и как применять в реальных примерах
LinkedIn: Sergey Drozdov
Boosty
Donate to support the project
GitHub account
GitHub profile