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; /// /// 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。 /// 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(options => options.UseSqlite(_connection)); builder.Services.AddCloudSyncServer(); var app = builder.Build(); using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); 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; }