();
db.Database.EnsureCreated();
}
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// === GREEN:按 Program.cs 硬约束顺序启用静态文件中间件 + SPA fallback ===
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
app.UseDynamicApi();
// SPA fallback(在 UseDynamicApi 之后,不覆盖 /api/*)
app.MapFallbackToFile("index.html");
app.StartAsync().GetAwaiter().GetResult();
return app;
}
[Fact]
public async Task Root_Get_ReturnsIndexHtml_WithAppMountDiv()
{
// Given: Host 启动且 WebRoot/wwwroot 有 index.html
// When : HTTP GET /
// Then : 200 + text/html + body 含 挂载点
using var response = await _client.GetAsync("/");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.StartsWith("text/html",
response.Content.Headers.ContentType?.MediaType ?? string.Empty,
StringComparison.OrdinalIgnoreCase);
var body = await response.Content.ReadAsStringAsync();
Assert.Contains(SpaMountPoint, body, StringComparison.Ordinal);
Assert.Contains(SpaTitle, body, StringComparison.Ordinal);
}
[Fact]
public async Task SpaRoute_Settings_Get_FallsBackToIndexHtml_Not404()
{
// Given: 同上
// When : HTTP GET /settings(前端内部路由)
// Then : 200 + body 是 index.html(注意:404 是 RED 时的现状,也是本测试在 RED 阶段失败的原因)
using var response = await _client.GetAsync("/settings");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
Assert.Contains(SpaMountPoint, body, StringComparison.Ordinal);
}
[Fact]
public async Task ApiDynamicApiRoute_PostProbe_NotOverriddenByFallback()
{
// Given: 同上
// When : HTTP POST /api/cloud-sync/probe(匿名 DynamicApi 端点)
// Then : 返回 JSON(body 含 isReachable 字段),不是 index.html
// 不要求 isReachable=true,因为 test 里 Host 没监听 5173,必 false;也可能 503,但不是 HTML。
using var response = await _client.PostAsJsonAsync("/api/cloud-sync/probe",
new ProbeRequest { TargetUrl = "http://localhost:5173" });
// 状态码不是 404 / 不返回 fallback 的 HTML:
Assert.NotEqual(HttpStatusCode.NotFound, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
Assert.DoesNotContain(SpaMountPoint, body, StringComparison.Ordinal);
// 至少含 DynamicApi 的 JSON 特征:isReachable 字段
Assert.Contains("isReachable", body, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task SwaggerUi_Get_Returns200_NotOverriddenByFallback()
{
// Given: 开发环境,Swagger 启用
// When : HTTP GET /swagger/index.html
// Then : 200,body 含 Swagger UI 的 "Swagger UI" 或 "swagger-ui",不是前端 index.html
using var response = await _client.GetAsync("/swagger/index.html");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
Assert.Contains("swagger", body, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain(SpaMountPoint, body, StringComparison.Ordinal);
}
public void Dispose()
{
_client.Dispose();
try { _app.StopAsync().GetAwaiter().GetResult(); } catch { /* 忽略 */ }
try { _app.DisposeAsync().GetAwaiter().GetResult(); } catch { /* 忽略 */ }
_connection.Dispose();
// 清理临时写入的 index.html(只删我们自己写的,不删目录里的其他可能文件)
try
{
var idx = Path.Combine(_webRootDir, "index.html");
if (File.Exists(idx)) File.Delete(idx);
}
catch { /* 忽略临时文件清理失败 */ }
}
}