簡體   English   中英

C# NetCore WebAPI 集成測試:HttpClient 使用 HTTPS 進行 GET 請求,但隨后僅使用 HTTP 進行 POST 請求,並失敗並出現 Forbidden

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

我正在使用 Net Core 6 開發一些非常簡單的 API 端點。與這些端點一起,我想為它們實現集成測試。

對於集成測試,我將xunitMicrosoft.AspNetCore.Mvc.Testing一起使用,特別是WebApplicationFactory來設置 SUT。

到目前為止,我有兩個 API 端點:

  1. Ping:接受 GET。 總是返回“pong”。
  2. 登錄:接受 POST。 實現登錄功能。

問題:Ping 請求測試成功。 SUT 似乎設置正確,HttpClient 確實請求,我得到了預期的結果“pong”。 但是,當我為 SignIn 運行測試時,SUT 似乎運行正常,但POST 請求永遠不會到達后端。 請求的響應是 403 Forbidden,而不是預期的 401 Unauthorized。

到目前為止我的調試:

  1. SignIn 和 Ping 端點按預期工作。 當我在 Kestrel 上運行 WebAPI 項目並從 Postman 發出請求時,我得到了預期的結果。 問題出在集成測試內部。
  2. 兩個測試中 HttpClient 的基本 URL 應該是 http://localhost。 執行 Ping 測試時,RequestUri 隨即更改為 https://localhost。 我認為發生這種情況是因為我有app.UseHttpsRedirection() 但是,在執行 SignUp 測試時,RequestUri 不會更改為 https。 我認為這是 403 響應的原因,但到目前為止我還沒有解決這個問題

PingTest 的 HTTP 響應

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}

SigInTest 的 HTTP 響應:

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}

我很感激你,我的電腦伙伴,可以提供的任何幫助。

我的代碼:

程序.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 端點)

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(集成測試)

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!
    }
}

json內容.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")
    { }
}

我最終放棄並通過創建帶有一些額外配置的 httpClient 來解決它,即

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

現在所有測試都按預期工作。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM