140 lines
5.6 KiB
C#
140 lines
5.6 KiB
C#
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);
|
||
}
|
||
}
|