140 lines
5.6 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.

namespace BuildFlow.Architecture.Tests;
using System.Xml.Linq;
using Xunit;
/// <summary>
/// BuildFlow Host 架构约束测试。确保后台只有 BuildFlow Host 一个启动入口,
/// 业务模块(Domain/Application/Infrastructure 三层)不能退化为独立进程。
/// HTTP 层已统一并入 BuildFlow.Host,模块不再保留 Api 类库。
/// </summary>
public class HostArchitectureTests
{
[Fact]
public void BusinessModulesShouldNotContainStandaloneProgramEntryPoints()
{
var repoRoot = FindRepoRoot();
var moduleRoot = Path.Combine(repoRoot.FullName, "src", "modules");
var programFiles = Directory
.EnumerateFiles(moduleRoot, "Program.cs", SearchOption.AllDirectories)
.Where(path => !ContainsPathSegment(path, "bin") && !ContainsPathSegment(path, "obj"))
.Where(path => !IsSsoPath(repoRoot, path))
.Select(path => Path.GetRelativePath(repoRoot.FullName, path).Replace('\\', '/'))
.Order(StringComparer.Ordinal)
.ToArray();
Assert.True(
programFiles.Length == 0,
"业务模块不允许包含独立 Program.cs,后台应只由 src/BuildFlow.Host/Program.cs 统一启动。当前残留:" +
Environment.NewLine +
string.Join(Environment.NewLine, programFiles));
}
[Fact]
public void BusinessModulesShouldNotContainLaunchProfiles()
{
var repoRoot = FindRepoRoot();
var moduleRoot = Path.Combine(repoRoot.FullName, "src", "modules");
var launchProfiles = Directory
.EnumerateFiles(moduleRoot, "launchSettings.json", SearchOption.AllDirectories)
.Where(path => !ContainsPathSegment(path, "bin") && !ContainsPathSegment(path, "obj"))
.Where(path => !IsSsoPath(repoRoot, path))
.Select(path => Path.GetRelativePath(repoRoot.FullName, path).Replace('\\', '/'))
.Order(StringComparer.Ordinal)
.ToArray();
Assert.True(
launchProfiles.Length == 0,
"业务模块不允许保留独立启动配置 launchSettings.json,启动配置应集中在 BuildFlow Host。当前残留:" +
Environment.NewLine +
string.Join(Environment.NewLine, launchProfiles));
}
[Fact]
public void BusinessModulesShouldNotHaveStandaloneApiProjects()
{
var repoRoot = FindRepoRoot();
var moduleRoot = Path.Combine(repoRoot.FullName, "src", "modules");
// 合并后,业务模块(rag/file/im/workflow)不应再有 Api 类库项目;仅 SSO 保留独立 Api
var apiProjects = Directory
.EnumerateFiles(moduleRoot, "*.Api.csproj", SearchOption.AllDirectories)
.Concat(Directory.EnumerateFiles(moduleRoot, "*Api.csproj", SearchOption.AllDirectories))
.Where(path => !ContainsPathSegment(path, "bin") && !ContainsPathSegment(path, "obj"))
.Where(path => !IsSsoPath(repoRoot, path))
.Distinct(StringComparer.Ordinal)
.Select(path => Path.GetRelativePath(repoRoot.FullName, path).Replace('\\', '/'))
.Order(StringComparer.Ordinal)
.ToArray();
Assert.True(
apiProjects.Length == 0,
"业务模块不允许保留独立 Api 类库项目(HTTP 层已统一并入 BuildFlow.Host)。当前残留:" +
Environment.NewLine +
string.Join(Environment.NewLine, apiProjects));
}
[Fact]
public void SsoApiShouldRemainStandaloneIdentityProvider()
{
var repoRoot = FindRepoRoot();
var ssoApiDir = Path.Combine(repoRoot.FullName, "src", "modules", "sso", "Api");
var ssoProjectPath = Path.Combine(ssoApiDir, "SSO.Api.csproj");
var violations = new List<string>();
if (!File.Exists(Path.Combine(ssoApiDir, "Program.cs")))
{
violations.Add("src/modules/sso/Api/Program.cs");
}
if (HasLibraryOutputType(ssoProjectPath))
{
violations.Add("src/modules/sso/Api/SSO.Api.csproj 设置了 <OutputType>Library</OutputType>");
}
Assert.True(
violations.Count == 0,
"SSO 是独立身份提供方,不归 BuildFlow Host 合并启动;它必须保留自己的进程入口。当前问题:" +
Environment.NewLine +
string.Join(Environment.NewLine, violations));
}
private static DirectoryInfo FindRepoRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
if (File.Exists(Path.Combine(current.FullName, "BuildFlow.slnx")))
{
return current;
}
current = current.Parent;
}
throw new InvalidOperationException("无法定位 BuildFlow.slnx,架构测试无法确定仓库边界。");
}
private static bool ContainsPathSegment(string path, string segment)
{
return path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
.Any(part => string.Equals(part, segment, StringComparison.Ordinal));
}
private static bool HasLibraryOutputType(string projectPath)
{
var document = XDocument.Load(projectPath);
return document
.Descendants("OutputType")
.Any(element => string.Equals(element.Value.Trim(), "Library", StringComparison.Ordinal));
}
private static bool IsSsoPath(DirectoryInfo repoRoot, string path)
{
var relativePath = Path.GetRelativePath(repoRoot.FullName, path).Replace('\\', '/');
return relativePath.StartsWith("src/modules/sso/", StringComparison.Ordinal);
}
}