using System.Net;
using System.Net.Http.Headers;
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.Http;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Xunit;
namespace Hua.Todo.Host.Tests.CloudSync;
///
/// 2026-08-08 用户反馈修复的 ATDD 验收测试:
/// 1. [根路径 404] 前端未构建(wwwroot/spa 无 index.html)时,访问 / 和 /settings 不 404,
/// 返回 MinimalBootstrapPageMiddleware 的最小引导页(含中文错误提示 + 构建命令)
/// 2. [无权限跳转登录] 浏览器 Accept: text/html 直访需要权限的 /api/*,DynamicApi 401/403
/// 应 302 跳 "/"(Host 双角色统一部署:前端 SPA 加载后显示登录弹窗 CloudSyncSettingsDialog)
/// 3. [不破坏现有 API] Accept: application/json 的 axios/fetch 调用需要权限的 /api/*,
/// 返回原有的 401/403 JSON(不 302,否则前端拦截器无法判断)
///
public sealed class HostBootstrapAndAuthRedirectTests : IDisposable
{
private readonly SqliteConnection _connection;
private readonly string _baseDir;
private readonly string _wwwrootDir;
private readonly HttpClient _client;
private readonly WebApplication _app;
public HostBootstrapAndAuthRedirectTests()
{
_baseDir = AppContext.BaseDirectory;
_wwwrootDir = Path.Combine(_baseDir, "wwwroot");
Directory.CreateDirectory(_wwwrootDir);
// 模拟"前端未构建"场景:删掉 wwwroot/spa/index.html(如果存在)
var idx = Path.Combine(_wwwrootDir, "spa", "index.html");
if (File.Exists(idx)) File.Delete(idx);
_connection = new SqliteConnection("Data Source=HostBootstrapAuthRedirectTests;Mode=Memory;Cache=Shared");
_connection.Open();
_app = BuildHost(environmentName: "Development");
_client = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false })
{
BaseAddress = new Uri(_app.Urls.First()),
};
}
private WebApplication BuildHost(string environmentName)
{
var builder = WebApplication.CreateBuilder();
builder.Environment.EnvironmentName = environmentName;
builder.WebHost.UseUrls("http://127.0.0.1:0");
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddApplicationServices("Data Source=HostBootstrapAuthRedirectTests;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();
}
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// 与 Program.cs 完全对齐:MinimalBootstrapPageMiddleware 放在 UseDefaultFiles/UseStaticFiles 之前
// (注意:本测试类中 Program.cs 里的内部类型 MinimalBootstrapPageMiddleware 不可见,
// 这里用等价实现的内联委托替代,保持相同判断规则 + 相同 HTML 特征字)
app.Use(async (context, next) =>
{
await next();
// 相同判断规则:
var indexHtmlPath = Path.Combine(app.Environment.WebRootPath, "spa", "index.html");
if (File.Exists(indexHtmlPath)) return;
if (context.Response.StatusCode != StatusCodes.Status404NotFound) return;
var method = context.Request.Method;
if (!HttpMethods.IsGet(method) && !HttpMethods.IsHead(method)) return;
var path = context.Request.Path.Value ?? string.Empty;
if (path.StartsWith("/api/", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("/swagger", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("/mcp", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("/admin/", StringComparison.OrdinalIgnoreCase))
return;
var accept = context.Request.Headers["Accept"].ToString();
if (string.IsNullOrEmpty(accept) ||
!accept.Contains("text/html", StringComparison.OrdinalIgnoreCase))
return;
context.Response.StatusCode = StatusCodes.Status200OK;
context.Response.ContentType = "text/html; charset=utf-8";
// 特征字:用于测试断言,不需要完整样式
await context.Response.WriteAsync(
"Hua.Todo - 前端尚未构建" +
"前端尚未构建" +
"dotnet build src/Hua.Todo.Host