✨ 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 进度与待办清单
This commit is contained in:
14
.claude/settings.local.json
Normal file
14
.claude/settings.local.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git commit -m ':*)",
|
||||
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln -v minimal 2>&1')",
|
||||
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test tests/Flyshot.Server.IntegrationTests/Flyshot.Server.IntegrationTests.csproj -v minimal 2>&1')",
|
||||
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test FlyshotReplacement.sln --no-build -v minimal 2>&1')",
|
||||
"Bash(DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln --no-restore -v minimal)",
|
||||
"Bash(DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test tests/Flyshot.Server.IntegrationTests/Flyshot.Server.IntegrationTests.csproj --no-build -v minimal)",
|
||||
"Bash(python -c \"import json; json.load\\(open\\('.vscode/launch.json'\\)\\); json.load\\(open\\('.vscode/tasks.json'\\)\\); print\\('JSON valid.'\\)\")",
|
||||
"Bash(python -c \"import json; json.load\\(open\\('.vscode/launch.json', encoding='utf-8'\\)\\); json.load\\(open\\('.vscode/tasks.json', encoding='utf-8'\\)\\); print\\('JSON valid.'\\)\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
67
.vscode/launch.json
vendored
Normal file
67
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
// VS Code 启动与调试配置
|
||||
// 依赖 C# 扩展(OmniSharp 或 C# Dev Kit)提供 coreclr 调试器。
|
||||
// 文档:https://code.visualstudio.com/docs/csharp/debugger-settings
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// 标准调试启动:编译并启动 Host,命中断点,浏览器自动打开首页
|
||||
"name": ".NET Core Launch (Host)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/src/Flyshot.Server.Host/Flyshot.Server.Host.csproj",
|
||||
"--no-launch-profile"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_URLS": "http://localhost:5190"
|
||||
},
|
||||
"stopAtEntry": false,
|
||||
"console": "internalConsole",
|
||||
"preLaunchTask": "build",
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "\\bNow listening on:\\s+(https?://\\S+)",
|
||||
"uriFormat": "%s"
|
||||
}
|
||||
},
|
||||
{
|
||||
// 热重载调试启动:自动编译、自动重启、断点保留;迭代 Web / 控制器层时首选
|
||||
"name": ".NET Core Watch (Host)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/src/Flyshot.Server.Host/Flyshot.Server.Host.csproj",
|
||||
"--no-launch-profile"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_URLS": "http://localhost:5190"
|
||||
},
|
||||
"stopAtEntry": false,
|
||||
"console": "integratedTerminal",
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "\\bNow listening on:\\s+(https?://\\S+)",
|
||||
"uriFormat": "%s"
|
||||
}
|
||||
},
|
||||
{
|
||||
// 附加到正在运行的 dotnet 进程(如已手动 `dotnet run` 或 Windows Service 模式)
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach",
|
||||
"processId": "${command:pickProcess}"
|
||||
}
|
||||
]
|
||||
}
|
||||
160
.vscode/tasks.json
vendored
Normal file
160
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
{
|
||||
// VS Code 任务配置
|
||||
// 文档:https://code.visualstudio.com/docs/editor/tasks
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
// 构建整个解决方案,是 launch.json 启动前的默认 preLaunchTask
|
||||
"label": "build",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/FlyshotReplacement.sln",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary",
|
||||
"-v",
|
||||
"minimal"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"problemMatcher": "$msCompile",
|
||||
"presentation": {
|
||||
"reveal": "silent",
|
||||
"clear": true
|
||||
}
|
||||
},
|
||||
{
|
||||
// 仅构建宿主项目,迭代 Web 层时比整解决方案快
|
||||
"label": "build-host",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/src/Flyshot.Server.Host/Flyshot.Server.Host.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary",
|
||||
"-v",
|
||||
"minimal"
|
||||
],
|
||||
"group": "build",
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
// 还原 NuGet 包,新增引用或克隆后第一次打开时使用
|
||||
"label": "restore",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"restore",
|
||||
"${workspaceFolder}/FlyshotReplacement.sln"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
// 清理所有项目的 bin/obj
|
||||
"label": "clean",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"clean",
|
||||
"${workspaceFolder}/FlyshotReplacement.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
// 跑全部测试(领域 + 集成)
|
||||
"label": "test",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"test",
|
||||
"${workspaceFolder}/FlyshotReplacement.sln",
|
||||
"--no-restore",
|
||||
"-v",
|
||||
"minimal"
|
||||
],
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
// 仅跑领域 / 算法层测试,迭代规划逻辑时使用
|
||||
"label": "test-core",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"test",
|
||||
"${workspaceFolder}/tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj",
|
||||
"-v",
|
||||
"minimal"
|
||||
],
|
||||
"group": "test",
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
// 仅跑宿主集成测试,迭代 HTTP / 控制器层时使用
|
||||
"label": "test-integration",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"test",
|
||||
"${workspaceFolder}/tests/Flyshot.Server.IntegrationTests/Flyshot.Server.IntegrationTests.csproj",
|
||||
"-v",
|
||||
"minimal"
|
||||
],
|
||||
"group": "test",
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
// 启动宿主,供 launch.json 的 watch 配置作为前置任务
|
||||
"label": "watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/src/Flyshot.Server.Host/Flyshot.Server.Host.csproj",
|
||||
"--launch-profile",
|
||||
"http"
|
||||
],
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "dotnet-watch",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^.*$",
|
||||
"file": 1,
|
||||
"location": 2,
|
||||
"message": 3
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "^.*Watch run started.*$",
|
||||
"endsPattern": "^.*Application started.*$"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
// Release 配置发布到 publish/,用于现场部署包打包
|
||||
"label": "publish",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"publish",
|
||||
"${workspaceFolder}/src/Flyshot.Server.Host/Flyshot.Server.Host.csproj",
|
||||
"-c",
|
||||
"Release",
|
||||
"-o",
|
||||
"${workspaceFolder}/publish"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
- [x] 完成 ICSP 轨迹导出结果与旧系统对齐
|
||||
- [x] 将 `ExecuteTrajectory` / `ExecuteFlyShotTraj` 接入 FANUC 运行时链路
|
||||
- [x] 落地 Web 状态页
|
||||
- [x] 落地浏览器内 OpenAPI 自动驱动的接口调试页(`/debug`),与 `Swagger:Enabled` 同步可见
|
||||
- [x] 固化 `10010 / 10012 / 60015` FANUC 基础协议帧编解码,确认 `10010` 状态帧为 90B
|
||||
- [x] 使用本地 TCP/UDP 模拟器覆盖命令通道、状态通道和 J519 基础收发
|
||||
- [x] 补齐 `Get/SetSpeedRatio`、`Get/SetTCP`、`Get/SetIO` 真机命令体与响应解析
|
||||
@@ -47,13 +48,14 @@
|
||||
1. 配置与测试基线
|
||||
- [x] 修正 `ConfigCompatibilityTests` 当前样本路径漂移:`Rvbust/EOL10_EAU_0/RobotConfig.json` 不再包含 `001`,应改用稳定样本或更新断言。
|
||||
- [x] 将 `RobotConfig.json` 中的 `use_do`、`io_keep_cycles`、`acc_limit`、`jerk_limit`、`adapt_icsp_try_num` 全部贯通到规划和执行链路。
|
||||
- [ ] 为新 HTTP API 补一份当前现场调用顺序文档,替代旧 `ControllerClient` 工作流。
|
||||
- [ ] 为新 HTTP API 补一份当前现场调用顺序文档,替代旧 `ControllerClient` 工作流(`/debug` 页已提供交互式覆盖,仍需补静态文档说明现场调用顺序)。
|
||||
|
||||
2. 轨迹规划
|
||||
- [x] 补齐 ICSP 最终 `global_scale > 1.0` 失败判定,避免未收敛轨迹被当作有效结果执行。
|
||||
- [x] 将 self-adapt-icsp 的补点次数改为使用配置中的 `adapt_icsp_try_num`。
|
||||
- [ ] 如果现场仍需要 `method="doubles"`,实现 `TrajectoryDoubleS` 等价规划;否则在 HTTP 文档中明确标为不支持。
|
||||
- [ ] 把已完成对齐的旧系统轨迹样本固化为 golden tests,防止后续重构破坏轨迹一致性。
|
||||
- [ ] 补齐 `save_traj` / `SaveTrajInfo` 的规划结果导出,将稠密关节轨迹、笛卡尔轨迹和 ShotEvents 写入可诊断 artifacts。
|
||||
|
||||
3. FANUC TCP 10012 命令通道
|
||||
- [x] 补齐 `GetSpeedRatio` / `SetSpeedRatio` 真机命令体与响应解析。
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"robot": {
|
||||
"use_do": false,
|
||||
"io_addr": [],
|
||||
"io_keep_cycles": 2,
|
||||
"acc_limit": 1,
|
||||
"jerk_limit": 1,
|
||||
"adapt_icsp_try_num": 5
|
||||
},
|
||||
"flying_shots": {}
|
||||
{
|
||||
"robot": {
|
||||
"use_do": false,
|
||||
"io_addr": [],
|
||||
"io_keep_cycles": 2,
|
||||
"acc_limit": 1,
|
||||
"jerk_limit": 1,
|
||||
"adapt_icsp_try_num": 5
|
||||
},
|
||||
"flying_shots": {}
|
||||
}
|
||||
1207
src/Flyshot.Server.Host/Controllers/DebugConsoleController.cs
Normal file
1207
src/Flyshot.Server.Host/Controllers/DebugConsoleController.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ namespace Flyshot.Server.Host.Controllers;
|
||||
/// 提供宿主基础探活与诊断接口。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Tags("基础与状态")]
|
||||
public sealed class HealthController : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace Flyshot.Server.Host.Controllers;
|
||||
/// 提供对 `flyshot-uaes-interface` 既有 FastAPI HTTP 路由层的一比一 MVC 兼容控制器。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Tags("ControllerClient 兼容")]
|
||||
public sealed class LegacyHttpApiController : ControllerBase
|
||||
{
|
||||
private readonly IControllerClientCompatService _compatService;
|
||||
@@ -431,6 +432,8 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
/// 兼容旧 `/execute_trajectory/` 路由,并接受两种历史请求体形状。
|
||||
/// </summary>
|
||||
/// <param name="waypoints">轨迹请求体。</param>
|
||||
/// <param name="method">查询字符串中的 method 覆盖值(兼容历史调用方式)。</param>
|
||||
/// <param name="save_traj">查询字符串中的 save_traj 覆盖值(兼容历史调用方式)。</param>
|
||||
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
|
||||
[HttpPost("/execute_trajectory/")]
|
||||
public IActionResult ExecuteTrajectory(
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace Flyshot.Server.Host.Controllers;
|
||||
/// 提供只读状态监控页面和控制器状态快照 API。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Tags("基础与状态")]
|
||||
public sealed class StatusController : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
@@ -84,6 +85,30 @@ public sealed class StatusController : ControllerBase
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* 顶部操作区按钮和外链按钮共用同一组视觉样式,便于现场顺手跳转。 */
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.link-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 36px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
font: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link-button:hover {
|
||||
background: rgba(0, 124, 137, 0.08);
|
||||
}
|
||||
|
||||
main {
|
||||
width: min(1180px, calc(100% - 32px));
|
||||
margin: 22px auto;
|
||||
@@ -216,7 +241,10 @@ public sealed class StatusController : ControllerBase
|
||||
<header>
|
||||
<div class="topbar">
|
||||
<h1>Flyshot Replacement 状态监控</h1>
|
||||
<button id="refresh" type="button">刷新</button>
|
||||
<div class="actions">
|
||||
<a class="link-button" href="/debug" target="_blank" rel="noopener">调试接口</a>
|
||||
<button id="refresh" type="button">刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<!-- 生成 XML 文档以便 Swashbuckle 把控制器/DTO 上的 /// summary 注释注入 OpenAPI 文档,
|
||||
调试页和 Swagger UI 的端点标题都依赖这一份文档。1591 抑制掉 “缺失 XML 注释” 的噪音。 -->
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -2,6 +2,7 @@ using Flyshot.ControllerClientCompat;
|
||||
using Flyshot.Server.Host;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -19,6 +20,13 @@ builder.Services.AddSwaggerGen(options =>
|
||||
Title = swaggerOptions.Title,
|
||||
Version = swaggerOptions.Version
|
||||
});
|
||||
|
||||
// 把控制器与 DTO 上的 /// summary 注释纳入 OpenAPI 文档;调试页据此渲染端点标题。
|
||||
var xmlDocumentationPath = Path.Combine(AppContext.BaseDirectory, $"{typeof(Program).Assembly.GetName().Name}.xml");
|
||||
if (File.Exists(xmlDocumentationPath))
|
||||
{
|
||||
options.IncludeXmlComments(xmlDocumentationPath, includeControllerXmlComments: true);
|
||||
}
|
||||
});
|
||||
|
||||
var corsOptions = builder.Configuration.GetSection("Cors").Get<HostCorsOptions>() ?? new HostCorsOptions();
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
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"
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user