feat(server): 添加静态状态页与调试入口

- 将状态页、调试页改为 `wwwroot` 静态资源
  - 补充调试配置接口与前端脚本
  - 为兼容层、规划层和运行时补充日志
  - 更新集成测试覆盖新入口
This commit is contained in:
2026-04-29 14:05:02 +08:00
parent 0724efebed
commit c38faddbf0
27 changed files with 2630 additions and 1894 deletions

View File

@@ -5,54 +5,74 @@ using Microsoft.Extensions.Configuration;
namespace Flyshot.Server.IntegrationTests;
/// <summary>
/// 验证 `/debug` 在线 API 调试页的暴露策略与基础内容契约。
/// 验证 `wwwroot` 静态调试页和调试配置 API 的基础内容契约。
/// </summary>
/// <remarks>
/// 调试页与 Swagger UI 共用 <c>Swagger:Enabled</c> 开关,开关关闭时
/// 调试页与 `/swagger` 一同下线,避免生产环境意外暴露调试入口
/// 调试页自身是静态 HTML真正的 Swagger 地址由配置 API 下发;
/// 当 Swagger 关闭时,配置 API 返回 404前端据此显示不可用状态
/// </remarks>
public sealed class DebugConsoleEndpointTests(FlyshotServerFactory factory) : IClassFixture<FlyshotServerFactory>
{
private readonly FlyshotServerFactory _factory = factory;
/// <summary>
/// 当 Swagger 启用时,`/debug` 应当返回完整的 HTML 调试页
/// `debug.html` 应当作为可直接调试的静态页面暴露
/// </summary>
[Fact]
public async Task GetDebug_WhenSwaggerEnabled_ReturnsConsoleHtml()
public async Task GetDebugHtml_ReturnsConsoleStaticPage()
{
// 默认配置即开启 Swagger调试页应当作为浏览器可直接打开的 HTML 暴露。
using var configuredFactory = CreateFactoryWithSwaggerEnabled(true);
using var client = configuredFactory.CreateClient();
using var response = await client.GetAsync("/debug");
using var response = await client.GetAsync("/debug.html");
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);
Assert.Contains("/assets/debug.css", html, StringComparison.Ordinal);
Assert.Contains("/assets/debug.js", html, StringComparison.Ordinal);
using var scriptResponse = await client.GetAsync("/assets/debug.js");
Assert.Equal(HttpStatusCode.OK, scriptResponse.StatusCode);
var script = await scriptResponse.Content.ReadAsStringAsync();
Assert.Contains("/api/debug/config", script, StringComparison.Ordinal);
}
/// <summary>
/// 当 Swagger 关闭时,`/debug` 应当与 `/swagger` 同步下线404
/// 当 Swagger 启用时,调试配置 API 应当返回实际 Swagger JSON 地址
/// </summary>
[Fact]
public async Task GetDebug_WhenSwaggerDisabled_ReturnsNotFound()
public async Task GetDebugConfig_WhenSwaggerEnabled_ReturnsSwaggerJsonUrl()
{
using var configuredFactory = CreateFactoryWithSwaggerEnabled(true);
using var client = configuredFactory.CreateClient();
using var response = await client.GetAsync("/api/debug/config");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
await using var responseStream = await response.Content.ReadAsStreamAsync();
using var document = await System.Text.Json.JsonDocument.ParseAsync(responseStream);
Assert.Equal("/swagger/v1/swagger.json", document.RootElement.GetProperty("swaggerJsonUrl").GetString());
}
/// <summary>
/// 当 Swagger 关闭时,调试配置 API 应当与 Swagger UI 同步下线404
/// </summary>
[Fact]
public async Task GetDebugConfig_WhenSwaggerDisabled_ReturnsNotFound()
{
// 显式把 Swagger:Enabled 置为 false此时调试页也不应当被访问到。
using var configuredFactory = CreateFactoryWithSwaggerEnabled(false);
using var client = configuredFactory.CreateClient();
using var response = await client.GetAsync("/debug");
using var response = await client.GetAsync("/api/debug/config");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
@@ -81,21 +101,20 @@ public sealed class DebugConsoleEndpointTests(FlyshotServerFactory factory) : IC
}
/// <summary>
/// 状态页应当提供跳转到调试页的入口,便于现场顺手跳转。
/// 状态页应当提供跳转到静态调试页的入口,便于现场顺手跳转。
/// </summary>
[Fact]
public async Task GetStatusPage_LinksToDebugConsole()
public async Task GetStatusHtml_LinksToDebugConsole()
{
using var configuredFactory = CreateFactoryWithSwaggerEnabled(true);
using var client = configuredFactory.CreateClient();
using var response = await client.GetAsync("/status");
using var response = await client.GetAsync("/status.html");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var html = await response.Content.ReadAsStringAsync();
// 状态页头部需要至少一个指向 `/debug` 的链接,文案不强制以保留排版调整空间。
Assert.Contains("href=\"/debug\"", html, StringComparison.Ordinal);
Assert.Contains("href=\"/debug.html\"", html, StringComparison.Ordinal);
}
/// <summary>

View File

@@ -10,21 +10,28 @@ namespace Flyshot.Server.IntegrationTests;
public sealed class StatusEndpointTests(FlyshotServerFactory factory) : IClassFixture<FlyshotServerFactory>
{
/// <summary>
/// 验证状态页返回可由浏览器直接打开的 HTML并引用状态快照 API。
/// 验证状态页作为 wwwroot 静态 HTML 暴露,并引用状态快照 API。
/// </summary>
[Fact]
public async Task GetStatusPage_ReturnsMonitoringHtml()
public async Task GetStatusHtml_ReturnsMonitoringStaticPage()
{
using var client = factory.CreateClient();
using var response = await client.GetAsync("/status");
using var response = await client.GetAsync("/status.html");
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("/api/status/snapshot", html, StringComparison.Ordinal);
Assert.Contains("/assets/status.css", html, StringComparison.Ordinal);
Assert.Contains("/assets/status.js", html, StringComparison.Ordinal);
using var scriptResponse = await client.GetAsync("/assets/status.js");
Assert.Equal(HttpStatusCode.OK, scriptResponse.StatusCode);
var script = await scriptResponse.Content.ReadAsStringAsync();
Assert.Contains("/api/status/snapshot", script, StringComparison.Ordinal);
}
/// <summary>