SmartOpsTrustedScopeTests.cs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. using Admin.NET.Plugin.AiDOP.Controllers;
  2. using Admin.NET.Plugin.AiDOP.Infrastructure;
  3. using Admin.NET.Plugin.AiDOP.Service;
  4. using Admin.NET.Plugin.AiDOP.SmartOps;
  5. using Xunit;
  6. namespace Admin.NET.Plugin.AiDOP.Tests.SmartOps;
  7. public class SmartOpsTrustedScopeTests
  8. {
  9. private const long TenantA = 838257186181189;
  10. private const long FactoryA = 838257186320453;
  11. private const long FactoryB = 838257212858437;
  12. /// <summary>种子/脚本建的租户(如 Ai-DOP 演示公司),根组织与租户同号。</summary>
  13. private const long SeededTenant = 1300000000777;
  14. [Fact]
  15. public void PlatformTenant_IsRejected()
  16. {
  17. Assert.Throws<InvalidOperationException>(() =>
  18. SmartOpsTrustedScopeRules.EnsureBusinessTenant(SmartOpsTrustedScopeRules.PlatformTenantId));
  19. Assert.Throws<InvalidOperationException>(() =>
  20. SmartOpsTrustedScopeRules.EnsureBusinessTenant(0));
  21. Assert.Throws<InvalidOperationException>(() =>
  22. SmartOpsTrustedScopeRules.EnsureBusinessTenant(1));
  23. }
  24. [Fact]
  25. public void BusinessTenant_IsAccepted()
  26. {
  27. SmartOpsTrustedScopeRules.EnsureBusinessTenant(TenantA);
  28. }
  29. [Theory]
  30. [InlineData(838257186181189, 838257186320453, 1L, 838257186320453)]
  31. [InlineData(838257186181189, 838257186320453, 838257212858437L, 838257186320453)]
  32. [InlineData(838257186181189, 838257186320453, 838257186181189L, 838257186320453)]
  33. [InlineData(838257186181189, 838257186320453, 838257186320453L, 838257186320453)]
  34. public void ClientFactory_IsNeverUsedAsPermissionBoundary(
  35. long tenantId, long orgId, long candidate, long expected)
  36. {
  37. Assert.Equal(expected, SmartOpsTrustedScopeRules.ResolveFactory(tenantId, orgId, candidate));
  38. }
  39. [Fact]
  40. public void MissingOrg_IsRejected()
  41. {
  42. Assert.Throws<InvalidOperationException>(() =>
  43. SmartOpsTrustedScopeRules.ResolveFactory(TenantA, 0, FactoryA));
  44. }
  45. /// <summary>
  46. /// 种子/SQL 脚本建的租户,其根组织 Id 与租户 Id 同号(Admin.NET 官方约定,
  47. /// 见 SysTenantSeedData「Id=OrgId=DefaultTenantId」与 SysOrgSeedData 根组织)。
  48. /// 该形态是「已配置组织」,不得当成「未配置」拒绝。
  49. /// </summary>
  50. [Fact]
  51. public void SeededTenant_WhoseRootOrgSharesTenantId_IsAccepted()
  52. {
  53. Assert.Equal(SeededTenant, SmartOpsTrustedScopeRules.ResolveFactory(SeededTenant, SeededTenant, null));
  54. Assert.Equal(SeededTenant, SmartOpsTrustedScopeRules.ResolveFactory(SeededTenant, SeededTenant, FactoryA));
  55. }
  56. [Fact]
  57. public void AuthorizedFactory_MatchesTenantOrgOnly()
  58. {
  59. Assert.True(SmartOpsTrustedScopeRules.IsAuthorizedFactory(TenantA, FactoryA, FactoryA));
  60. Assert.False(SmartOpsTrustedScopeRules.IsAuthorizedFactory(TenantA, FactoryA, FactoryB));
  61. }
  62. [Fact]
  63. public void OperationalFactory_AcceptsLegacyDefaultAndTrustedOrg()
  64. {
  65. Assert.True(SmartOpsOperationalFactory.Matches(TenantA, FactoryA, 1));
  66. Assert.True(SmartOpsOperationalFactory.Matches(TenantA, FactoryA, FactoryA));
  67. Assert.True(SmartOpsOperationalFactory.Matches(TenantA, FactoryA, 0));
  68. Assert.True(SmartOpsOperationalFactory.Matches(TenantA, FactoryA, null));
  69. Assert.False(SmartOpsOperationalFactory.Matches(TenantA, FactoryA, TenantA));
  70. Assert.False(SmartOpsOperationalFactory.Matches(TenantA, FactoryA, FactoryB));
  71. }
  72. [Fact]
  73. public void PlanNo_IsTenantScopedAndMonotonic()
  74. {
  75. var prefix = SmartOpsImprovementPlanNo.BuildPrefix(new DateTime(2026, 8, 17));
  76. Assert.Equal("SOP-20260817-", prefix);
  77. Assert.Equal("SOP-20260817-001", SmartOpsImprovementPlanNo.Next(prefix, Array.Empty<string>()));
  78. Assert.Equal(
  79. "SOP-20260817-003",
  80. SmartOpsImprovementPlanNo.Next(prefix, new[] { "SOP-20260817-001", "SOP-20260817-002" }));
  81. }
  82. [Fact]
  83. public void ConcurrentPlanNo_DoesNotReuseExistingNumbers()
  84. {
  85. var prefix = "SOP-20260817-";
  86. var existing = new List<string>();
  87. var generated = new HashSet<string>();
  88. for (var i = 0; i < 20; i++)
  89. {
  90. var next = SmartOpsImprovementPlanNo.Next(prefix, existing);
  91. Assert.True(generated.Add(next));
  92. existing.Add(next);
  93. }
  94. Assert.Equal(20, generated.Count);
  95. Assert.Contains("SOP-20260817-020", generated);
  96. }
  97. [Fact]
  98. public void DiagnosisAndImprovement_UseTrustedScopeNotRequestTenant()
  99. {
  100. var diagnosis = typeof(AidopKanbanController).GetMethod(nameof(AidopKanbanController.GetSmartDiagnosis));
  101. Assert.NotNull(diagnosis);
  102. var controllerSource = ReadSiblingSource(
  103. typeof(AdoSmartOpsImprovementPlanController),
  104. "AdoSmartOpsImprovementPlanController.cs");
  105. var diagnosisSource = ReadSiblingSource(
  106. typeof(AidopKanbanController),
  107. "AidopKanbanController.Diagnosis.cs");
  108. Assert.DoesNotContain("AidopTenantHelper.GetTenantId", controllerSource);
  109. Assert.DoesNotContain("AidopTenantHelper.GetTenantId", diagnosisSource);
  110. Assert.Contains("_trustedScope.ResolveAsync", controllerSource);
  111. Assert.Contains("_trustedScope.ResolveAsync", diagnosisSource);
  112. Assert.Contains("x.TenantId == plan.TenantId", controllerSource);
  113. Assert.Contains("x.TenantId == tenantId", controllerSource);
  114. Assert.DoesNotContain("[AllowAnonymous]", controllerSource);
  115. Assert.DoesNotContain("FactoryId = input.FactoryId", controllerSource);
  116. }
  117. [Fact]
  118. public void ApproverGuard_ClearsFiltersWhenResolvingRoleMembers()
  119. {
  120. var source = ReadSiblingSource(
  121. typeof(SmartOpsImprovementApproverGuard),
  122. "SmartOpsImprovementApproverGuard.cs");
  123. Assert.Contains("Queryable<SysUserRole>()", source);
  124. Assert.Contains(".ClearFilter()", source);
  125. Assert.Contains("u.TenantId == tenantId", source);
  126. }
  127. [Fact]
  128. public void ApprovalCallback_UpdatesWithTenantCondition()
  129. {
  130. var source = ReadSiblingSource(
  131. typeof(SmartOpsImprovementBizHandler),
  132. "SmartOpsImprovementBizHandler.cs");
  133. Assert.Contains("x.TenantId == plan.TenantId", source);
  134. Assert.DoesNotContain(".Where(x => x.Id == bizId)", source.Replace("x.Id == bizId && x.TenantId == plan.TenantId", ""));
  135. }
  136. private static string ReadSiblingSource(Type type, string fileName)
  137. {
  138. var root = FindRepoRoot();
  139. var matches = Directory.GetFiles(root, fileName, SearchOption.AllDirectories)
  140. .Where(path => path.Replace('\\', '/').Contains("/Admin.NET.Plugin.AiDOP/")
  141. && !path.Contains(".Tests"))
  142. .ToList();
  143. Assert.NotEmpty(matches);
  144. return File.ReadAllText(matches[0]);
  145. }
  146. private static string FindRepoRoot()
  147. {
  148. var dir = new DirectoryInfo(AppContext.BaseDirectory);
  149. while (dir != null)
  150. {
  151. if (File.Exists(Path.Combine(dir.FullName, "AGENTS.md")))
  152. return dir.FullName;
  153. dir = dir.Parent;
  154. }
  155. return Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", ".."));
  156. }
  157. }