简体   繁体   中英

C# NetCore WebAPI integration testing: HttpClient uses HTTPS for GET request, but then uses only HTTP for the POST request, and fails with Forbidden

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")
    { }
}

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM