3213d4c718
- 重构架构:抽离AddCloudSyncServer到扩展类,补全中间层文档对齐 - 修复根路径404与鉴权跳转:新增中间件处理浏览器请求302跳转 - 实现种子数据初始化:添加默认管理员账号自动创建逻辑 - 优化前端代理与构建:新增Vite代理配置与自动构建Target - 补全测试与文档:新增集成测试,更新开发与部署文档
156 lines
6.2 KiB
C#
156 lines
6.2 KiB
C#
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;
|
||
}
|