* 新增 DebugConsoleController,提供 /debug 纯内嵌调试页 - 零外部依赖,基于 Swagger JSON 自动生成各端点表单 - 与 Swagger:Enabled 同步开关,避免生产环境误暴露 * 启用 <GenerateDocumentationFile>,将 XML 注释注入 OpenAPI - 调试页与 Swagger UI 共用同一份端点标题和说明 * 为 Health/Status/LegacyHttpApi 控制器添加 Tags 分组 * 补充 VS Code launch.json 与 tasks.json,支持现场调试 * 新增 DebugConsoleEndpointTests 覆盖调试页基础响应 * 同步更新 README 进度与待办清单
142 lines
6.3 KiB
C#
142 lines
6.3 KiB
C#
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"
|
||
});
|
||
});
|
||
});
|
||
}
|
||
}
|