Files
FlyShotHost/tests/Flyshot.Server.IntegrationTests/DebugConsoleEndpointTests.cs
yunxiao.zhu 0292e077ff feat(server): 添加浏览器内 OpenAPI 调试页及诊断入口
* 新增 DebugConsoleController,提供 /debug 纯内嵌调试页
  - 零外部依赖,基于 Swagger JSON 自动生成各端点表单
  - 与 Swagger:Enabled 同步开关,避免生产环境误暴露
* 启用 <GenerateDocumentationFile>,将 XML 注释注入 OpenAPI
  - 调试页与 Swagger UI 共用同一份端点标题和说明
* 为 Health/Status/LegacyHttpApi 控制器添加 Tags 分组
* 补充 VS Code launch.json 与 tasks.json,支持现场调试
* 新增 DebugConsoleEndpointTests 覆盖调试页基础响应
* 同步更新 README 进度与待办清单
2026-04-27 10:33:53 +08:00

142 lines
6.3 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 Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
namespace Flyshot.Server.IntegrationTests;
/// <summary>
/// 验证 `/debug` 在线 API 调试页的暴露策略与基础内容契约。
/// </summary>
/// <remarks>
/// 调试页与 Swagger UI 共用 <c>Swagger:Enabled</c> 开关,开关关闭时
/// 调试页与 `/swagger` 一同下线,避免生产环境意外暴露调试入口。
/// </remarks>
public sealed class DebugConsoleEndpointTests(FlyshotServerFactory factory) : IClassFixture<FlyshotServerFactory>
{
private readonly FlyshotServerFactory _factory = factory;
/// <summary>
/// 当 Swagger 启用时,`/debug` 应当返回完整的 HTML 调试页。
/// </summary>
[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);
}
/// <summary>
/// 当 Swagger 关闭时,`/debug` 应当与 `/swagger` 同步下线404
/// </summary>
[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);
}
/// <summary>
/// 调试页需要从 Swagger JSON 中读取所有端点,因此 Swagger 文档必须包含基础和兼容层的代表性路由。
/// </summary>
[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");
}
/// <summary>
/// 状态页应当提供跳转到调试页的入口,便于现场顺手跳转。
/// </summary>
[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);
}
/// <summary>
/// 检查 Swagger 文档中是否存在指定路径,兼容尾斜杠归一化两种形态。
/// </summary>
/// <param name="paths">OpenAPI 文档中的 paths 节点。</param>
/// <param name="route">期望存在的路由字符串。</param>
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}");
}
/// <summary>
/// 构造覆盖了 <c>Swagger:Enabled</c> 配置项的测试宿主工厂。
/// </summary>
/// <param name="enabled">期望的 Swagger 启用状态。</param>
/// <returns>已应用配置覆盖的测试工厂。</returns>
private WebApplicationFactory<Program> CreateFactoryWithSwaggerEnabled(bool enabled)
{
return _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration((_, configurationBuilder) =>
{
// 通过 InMemory 配置覆盖 appsettings.json 中的 Swagger 开关,避免修改磁盘文件。
configurationBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{
["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"
});
});
});
}
}