Files
Hua.Todo/test/Hua.Todo.Host.Tests/CloudSync/HostHttpPipelineAnonymousEndpointTests.cs
T
ShaoHua 3213d4c718 feat: 完成v1.3.0架构对齐与Web端根路径托管全量开发
- 重构架构:抽离AddCloudSyncServer到扩展类,补全中间层文档对齐
- 修复根路径404与鉴权跳转:新增中间件处理浏览器请求302跳转
- 实现种子数据初始化:添加默认管理员账号自动创建逻辑
- 优化前端代理与构建:新增Vite代理配置与自动构建Target
- 补全测试与文档:新增集成测试,更新开发与部署文档
2026-08-08 07:33:44 +08:00

156 lines
6.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Net;
using System.Net.Http.Json;
using Hua.Todo.Application.Common;
using Hua.Todo.Application.Repositories;
using Hua.Todo.Application.Services.CloudSync.Models;
using Hua.Todo.HttpApi.AspNetCore.CloudSync;
using Hua.Todo.HttpApi.AspNetCore.DynamicApi;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Hua.Todo.Host.Tests.CloudSync;
/// <summary>
/// Host HTTP 管道行为测试。
/// 针对用户反馈的「前端云同步保存并探测报 Request failed with status code 307」。
///
/// 正确探测链路(嵌入式模式 MAUI / Avalonia):
/// 前端 POST /api/cloud-sync/probe {targetUrl}
/// → Vite proxy (target=http://localhost:5057)
/// → MAUI / Avalonia EmbeddedWebServer
/// → CloudSyncProxyMiddleware / CloudProbeService
/// → 后端 HttpClient 探测 {targetUrl}/api
/// 307 若出现,来自前端请求本身被某一层 HTTPS 重定向(Host 侧 UseHttpsRedirection),
/// 而非探测响应中的 HTTP 状态码。
///
/// 纯 Host 独立模式(VITE_API_TARGET 指向 Host)也必须保证匿名端点 HTTP 下不 307。
/// 因此 Host Program.cs 在开发环境不启用 UseHttpsRedirection。
/// </summary>
public sealed class HostHttpPipelineAnonymousEndpointTests : IDisposable
{
private readonly SqliteConnection _connection;
private readonly HttpClient _clientDev; // 开发环境(修复后:不启用 UseHttpsRedirection)
private readonly HttpClient _clientProd; // 生产环境(显式启用 UseHttpsRedirection,仅回归验证)
private readonly WebApplication _appDev;
private readonly WebApplication _appProd;
public HostHttpPipelineAnonymousEndpointTests()
{
_connection = new SqliteConnection("Data Source=HostHttpPipelineTests;Mode=Memory;Cache=Shared");
_connection.Open();
_appDev = BuildHost(environmentName: "Development", enableHttpsRedirection: false);
_appProd = BuildHost(environmentName: "Production", enableHttpsRedirection: true);
_clientDev = new HttpClient { BaseAddress = new Uri(_appDev.Urls.First()) };
_clientProd = new HttpClient { BaseAddress = new Uri(_appProd.Urls.First()) };
// 自动跟随重定向会隐藏 307,手动关掉以捕获原始状态码
_clientDev = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false })
{ BaseAddress = new Uri(_appDev.Urls.First()) };
_clientProd = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false })
{ BaseAddress = new Uri(_appProd.Urls.First()) };
}
private WebApplication BuildHost(string environmentName, bool enableHttpsRedirection)
{
var builder = WebApplication.CreateBuilder();
builder.Environment.EnvironmentName = environmentName;
builder.WebHost.UseUrls("http://127.0.0.1:0");
builder.Services.AddApplicationServices("Data Source=HostHttpPipelineTests;Mode=Memory;Cache=Shared");
builder.Services.AddDbContext<TodoDbContext>(options => options.UseSqlite(_connection));
builder.Services.AddCloudSyncServer();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<TodoDbContext>();
db.Database.EnsureCreated();
}
// 与修复后的 Program.cs 逻辑一致:仅非开发环境启用 HTTPS 重定向
if (enableHttpsRedirection)
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseDynamicApi();
app.StartAsync().GetAwaiter().GetResult();
return app;
}
// ===== 开发环境(修复后):匿名端点 HTTP 下不得重定向 =====
[Fact]
public async Task DevEnv_ProbeEndpoint_Http_NoRedirect()
{
var response = await _clientDev.PostAsJsonAsync("/api/cloud-sync/probe",
new ProbeRequest { TargetUrl = "http://localhost:5173" });
Assert.NotEqual(HttpStatusCode.TemporaryRedirect, response.StatusCode);
Assert.False(IsRedirect(response.StatusCode));
}
[Fact]
public async Task DevEnv_AuthLoginEndpoint_Http_NoRedirect()
{
var response = await _clientDev.PostAsJsonAsync("/api/auth/login",
new { userName = "admin", password = "x" });
Assert.NotEqual(HttpStatusCode.TemporaryRedirect, response.StatusCode);
Assert.False(IsRedirect(response.StatusCode));
}
[Fact]
public async Task DevEnv_AuthBootstrapEndpoint_Http_NoRedirect()
{
var response = await _clientDev.PostAsJsonAsync("/api/auth/bootstrap",
new { userName = "admin", password = "Admin@123" });
Assert.NotEqual(HttpStatusCode.TemporaryRedirect, response.StatusCode);
Assert.False(IsRedirect(response.StatusCode));
}
[Fact]
public async Task DevEnv_CloudSyncProxySettingsEndpoint_Http_NoRedirect()
{
var response = await _clientDev.GetAsync("/api/cloud-sync-proxy-settings");
Assert.NotEqual(HttpStatusCode.TemporaryRedirect, response.StatusCode);
Assert.False(IsRedirect(response.StatusCode));
}
// ===== 生产环境:启用 HTTPS 重定向,则 307(回归测试) =====
[Fact]
public async Task ProdEnv_ProbeEndpoint_Http_Returns307TemporaryRedirect()
{
var response = await _clientProd.PostAsJsonAsync("/api/cloud-sync/probe",
new ProbeRequest { TargetUrl = "http://localhost:5173" });
// 生产启用 HTTPS 重定向 → HTTP POST 应触发 307。部署时由反向代理处理,此测试仅保证行为可预测。
Assert.Equal(HttpStatusCode.TemporaryRedirect, response.StatusCode);
}
public void Dispose()
{
_clientDev.Dispose();
_clientProd.Dispose();
foreach (var app in new[] { _appDev, _appProd })
{
app.StopAsync().GetAwaiter().GetResult();
app.DisposeAsync().GetAwaiter().GetResult();
}
_connection.Dispose();
}
private static bool IsRedirect(HttpStatusCode code) =>
(int)code is >= 300 and < 400;
}