3

I am working on some very simple API endpoints using Net Core 6. Along with these endpoints, I want to implement integration test for them.

For the integration tests, I am using xunit together with Microsoft.AspNetCore.Mvc.Testing, particularly WebApplicationFactory to set up the SUT.

So far, I have two API endpoints:

  1. Ping: Accepts GET. Always returns "pong".
  2. SignIn: Accepts POST. Implements a SignIn functionality.

PROBLEM: The test for the Ping request is successful. The SUT seems to be set up correctly, the HttpClient does de request, and I get the expected result "pong". HOWEVER, when I run the test for SignIn, the SUT seems to be running correctly, but the POST request never reaches the backend. The response of the request is a 403 Forbidden, instead of the expected 401 Unauthorized.

My debugging so far:

  1. The SignIn and Ping endpoints work as expected. When I run the WebAPI project on Kestrel, and issue requests from Postman, I get the expected results. The problem is inside the integration tests.
  2. The base URL for the HttpClient in both tests is supposed to be http://localhost. When the Ping test is executed, the RequestUri is then changed to https://localhost. I assume this happens because I have app.UseHttpsRedirection() . However, when the SignUp test is executed, the RequestUri is not changed to https. I think this is the cause of the 403 response, but so far I haven't been able to solve the issue.

The HTTP response for PingTest

response
{StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
  Content-Type: text/plain; charset=utf-8
}}
    Content: {System.Net.Http.StreamContent}
    Headers: {}
    IsSuccessStatusCode: true
    ReasonPhrase: "OK"
    RequestMessage: {Method: GET, RequestUri: 'https://localhost/api/template/ping', Version: 1.1, Content: <null>, Headers:
{
}}
    StatusCode: OK
    TrailingHeaders: {}
    Version: {1.1}

The HTTP response for SigInTest:

response
{StatusCode: 403, ReasonPhrase: 'Forbidden', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
  Content-Type: application/problem+json; charset=utf-8
}}
    Content: {System.Net.Http.StreamContent}
    Headers: {}
    IsSuccessStatusCode: false
    ReasonPhrase: "Forbidden"
    RequestMessage: {Method: POST, RequestUri: 'http://localhost/api/template/sign-in', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
  Content-Type: application/json; charset=utf-8
  Content-Length: 35
}}
    StatusCode: Forbidden
    TrailingHeaders: {}
    Version: {1.1}
    _content: {System.Net.Http.StreamContent}
    _disposed: false
    _headers: {}
    _reasonPhrase: null
    _requestMessage: {Method: POST, RequestUri: 'http://localhost/api/template/sign-in', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
  Content-Type: application/json; charset=utf-8
  Content-Length: 35
}}
    _statusCode: Forbidden
    _trailingHeaders: {}
    _version: {1.1}

I appreciate any help that you, my fellow computer guy, can offer.

My code:

Program.cs

using System.Text;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var services = builder.Services;

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection(); // <-- IMPORTANT

app.UseAuthorization();
app.UseAuthentication();

app.MapControllers();

app.Run();

public partial class Program { } 

TemplateController.cs (API endpoints)

namespace foo;

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[Route("api/template")]
[RequireHttps]
[ApiController]
public class TemplateController : ControllerBase
{
    public TemplateController(){    }

    [Route("sign-in")]
    [HttpPost]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
    public ActionResult<User> SignIn(AuthRequestDto credentials)
    {
        if (credentials == null) return Unauthorized("Missing credentials."); //Integration tests never reach this line. If I reach this line, I am good. 

        try
        {
            //Call to auth library 
            var (user, tokens) = _authService.SignIn(credentials.UserName, credentials.Password);
            //Handle library's response. 
            return Ok();
        }
        catch (ArgumentException e) //<-- Thrown when auth fails. Replies with a 401, not a 403 
        {
            return Unauthorized(e.Message);
        }
        catch (InvalidOperationException e)
        {
            return Problem(detail: e.Message, title: "Server error");
        }
    }

    [Route("ping")]
    [HttpGet]
    [ProducesResponseType(StatusCodes.Status200OK)]
    public ActionResult<string> Ping()
    {
        return Ok("pong");
    }
}

AuthRequestDto.cs

using System.ComponentModel.DataAnnotations;

namespace foo;
public sealed class AuthRequestDto
{
    [Required]
    public string UserName { get; set; } = string.Empty;
    [Required]
    public string Password { get; set; } = string.Empty;
}

TemplateControllerTest.cs (The integration tests)

using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using Xunit;

namespace AuthWebTemplateTest.Controllers;

public class TemplateControllerTest
{
    [Fact]
    public async void SignInTest()
    {
        var app = new WebApplicationFactory<Program>()
            .WithWebHostBuilder(builder => { }); 

        var client = app.CreateClient(); //<-- Get the HttpClient
        
        string userName = "foo";
        string password = "bar";

        var body = new JsonContent(new { userName, password }); //<-- JsonContent is a custom class. Definition is below
        var resource = "/api/template/sign-in";
        var response = await client.PostAsync(resource, body);
        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); //<-- response.StatusCode is 403 Forbidden instead
    }

    [Fact]
    public async void PingTest()
    {
        var app = new WebApplicationFactory<Program>()
            .WithWebHostBuilder(builder => { }); 

        var client = app.CreateClient();

        var response = await client.GetAsync("/api/template/ping");
        Assert.Equal(HttpStatusCode.OK, response.StatusCode); //Good!
        Assert.Equal("pong", await response.Content.ReadAsStringAsync()); //Also Good!
    }
}

JsonContent.cs

using Newtonsoft.Json;
using System.Net.Http;
using System.Text;

namespace AuthWebTemplateTest.util;
internal class JsonContent : StringContent
{
    public JsonContent(object obj)
        : base(JsonConvert.SerializeObject(obj), Encoding.UTF8, "application/json")
    { }
}
1
  • UPDATE: Instead of using ` var resource = "/api/template/sign-in";` in my test, I am using ` var resource = "HTTPS://localhost/api/template/sign-in";`. The test works. Still now idea why the https redirection does not occur for the POST request, but still happens for the GET request. Commented Feb 1, 2022 at 20:32

1 Answer 1

5

I finally gave up and settled it by creating the httpClient with some additional conf, namely

private const string HTTPS_BASE_URL = "https://localhost";
...
var client = _factory.CreateClient(new() { BaseAddress = new Uri(HTTPS_BASE_URL) });

Now all tests work as expected.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.