using System.Net; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; namespace Flyshot.Server.IntegrationTests; /// /// 验证 `/debug` 在线 API 调试页的暴露策略与基础内容契约。 /// /// /// 调试页与 Swagger UI 共用 Swagger:Enabled 开关,开关关闭时 /// 调试页与 `/swagger` 一同下线,避免生产环境意外暴露调试入口。 /// public sealed class DebugConsoleEndpointTests(FlyshotServerFactory factory) : IClassFixture { private readonly FlyshotServerFactory _factory = factory; /// /// 当 Swagger 启用时,`/debug` 应当返回完整的 HTML 调试页。 /// [Fact] public async Task GetDebug_WhenSwaggerEnabled_ReturnsConsoleHtml() { // 默认配置即开启 Swagger,调试页应当作为浏览器可直接打开的 HTML 暴露。 using var configuredFactory = CreateFactoryWithSwaggerEnabled(true); using var client = configuredFactory.CreateClient(); using var response = await client.GetAsync("/debug"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.StartsWith("text/html", response.Content.Headers.ContentType?.MediaType); var html = await response.Content.ReadAsStringAsync(); // 页面标题与稳定锚点用于回归保护:调试页骨架一旦丢失,测试立即报警。 Assert.Contains("Flyshot Replacement 接口调试", html, StringComparison.Ordinal); Assert.Contains("id=\"debug-console-app\"", html, StringComparison.Ordinal); // 控制器需要在返回 HTML 前把 Swagger JSON URL 注入到页面占位符里, // 否则前端无法在加载时拉取 OpenAPI 文档。 Assert.Contains("/swagger/v1/swagger.json", html, StringComparison.Ordinal); Assert.DoesNotContain("__SWAGGER_JSON_URL__", html, StringComparison.Ordinal); } /// /// 当 Swagger 关闭时,`/debug` 应当与 `/swagger` 同步下线(404)。 /// [Fact] public async Task GetDebug_WhenSwaggerDisabled_ReturnsNotFound() { // 显式把 Swagger:Enabled 置为 false,此时调试页也不应当被访问到。 using var configuredFactory = CreateFactoryWithSwaggerEnabled(false); using var client = configuredFactory.CreateClient(); using var response = await client.GetAsync("/debug"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } /// /// 调试页需要从 Swagger JSON 中读取所有端点,因此 Swagger 文档必须包含基础和兼容层的代表性路由。 /// [Fact] public async Task SwaggerDocument_ContainsRepresentativeRoutesForDebugConsole() { using var configuredFactory = CreateFactoryWithSwaggerEnabled(true); using var client = configuredFactory.CreateClient(); using var response = await client.GetAsync("/swagger/v1/swagger.json"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); await using var responseStream = await response.Content.ReadAsStreamAsync(); using var document = await System.Text.Json.JsonDocument.ParseAsync(responseStream); var paths = document.RootElement.GetProperty("paths"); // 这些路径分别覆盖:基础探活、状态快照、版本查询、上传飞拍轨迹四种典型形态。 AssertPathExists(paths, "/healthz"); AssertPathExists(paths, "/api/status/snapshot"); AssertPathExists(paths, "/get_server_version"); AssertPathExists(paths, "/upload_flyshot"); } /// /// 状态页应当提供跳转到调试页的入口,便于现场顺手跳转。 /// [Fact] public async Task GetStatusPage_LinksToDebugConsole() { using var configuredFactory = CreateFactoryWithSwaggerEnabled(true); using var client = configuredFactory.CreateClient(); using var response = await client.GetAsync("/status"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var html = await response.Content.ReadAsStringAsync(); // 状态页头部需要至少一个指向 `/debug` 的链接,文案不强制以保留排版调整空间。 Assert.Contains("href=\"/debug\"", html, StringComparison.Ordinal); } /// /// 检查 Swagger 文档中是否存在指定路径,兼容尾斜杠归一化两种形态。 /// /// OpenAPI 文档中的 paths 节点。 /// 期望存在的路由字符串。 private static void AssertPathExists(System.Text.Json.JsonElement paths, string route) { // OpenAPI 生成器会把部分尾斜杠路径规范化,这里同时接受两种形态。 var withSlash = route.EndsWith('/') ? route : route + "/"; var withoutSlash = route.EndsWith('/') ? route.TrimEnd('/') : route; Assert.True( paths.TryGetProperty(withSlash, out _) || paths.TryGetProperty(withoutSlash, out _), $"Swagger 文档应当包含路径 {route}"); } /// /// 构造覆盖了 Swagger:Enabled 配置项的测试宿主工厂。 /// /// 期望的 Swagger 启用状态。 /// 已应用配置覆盖的测试工厂。 private WebApplicationFactory CreateFactoryWithSwaggerEnabled(bool enabled) { return _factory.WithWebHostBuilder(builder => { builder.ConfigureAppConfiguration((_, configurationBuilder) => { // 通过 InMemory 配置覆盖 appsettings.json 中的 Swagger 开关,避免修改磁盘文件。 configurationBuilder.AddInMemoryCollection(new Dictionary { ["Swagger:Enabled"] = enabled ? "true" : "false", ["Swagger:DocumentName"] = "v1", ["Swagger:Title"] = "Flyshot Replacement HTTP API", ["Swagger:Version"] = "v1", ["Swagger:JsonRouteTemplate"] = "swagger/{documentName}/swagger.json", ["Swagger:RoutePrefix"] = "swagger" }); }); }); } }