SmartOpsTrustedScopeTests.cs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  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. [Fact]
  13. public void PlatformTenant_IsRejected()
  14. {
  15. Assert.Throws<InvalidOperationException>(() =>
  16. SmartOpsTrustedScopeRules.EnsureBusinessTenant(SmartOpsTrustedScopeRules.PlatformTenantId));
  17. Assert.Throws<InvalidOperationException>(() =>
  18. SmartOpsTrustedScopeRules.EnsureBusinessTenant(0));
  19. Assert.Throws<InvalidOperationException>(() =>
  20. SmartOpsTrustedScopeRules.EnsureBusinessTenant(1));
  21. }
  22. [Fact]
  23. public void BusinessTenant_IsAccepted()
  24. {
  25. SmartOpsTrustedScopeRules.EnsureBusinessTenant(TenantA);
  26. }
  27. [Theory]
  28. [InlineData(838257186181189, 838257186320453, 1L, 838257186320453)]
  29. [InlineData(838257186181189, 838257186320453, 838257212858437L, 838257186320453)]
  30. [InlineData(838257186181189, 838257186320453, 838257186181189L, 838257186320453)]
  31. [InlineData(838257186181189, 838257186320453, 838257186320453L, 838257186320453)]
  32. public void ClientFactory_IsNeverUsedAsPermissionBoundary(
  33. long tenantId, long orgId, long candidate, long expected)
  34. {
  35. Assert.Equal(expected, SmartOpsTrustedScopeRules.ResolveFactory(tenantId, orgId, candidate));
  36. }
  37. [Fact]
  38. public void MissingOrSelfFactory_IsRejected()
  39. {
  40. Assert.Throws<InvalidOperationException>(() =>
  41. SmartOpsTrustedScopeRules.ResolveFactory(TenantA, 0, FactoryA));
  42. Assert.Throws<InvalidOperationException>(() =>
  43. SmartOpsTrustedScopeRules.ResolveFactory(TenantA, TenantA, FactoryA));
  44. }
  45. [Fact]
  46. public void AuthorizedFactory_MatchesTenantOrgOnly()
  47. {
  48. Assert.True(SmartOpsTrustedScopeRules.IsAuthorizedFactory(TenantA, FactoryA, FactoryA));
  49. Assert.False(SmartOpsTrustedScopeRules.IsAuthorizedFactory(TenantA, FactoryA, FactoryB));
  50. }
  51. [Fact]
  52. public void OperationalFactory_AcceptsLegacyDefaultAndTrustedOrg()
  53. {
  54. Assert.True(SmartOpsOperationalFactory.Matches(TenantA, FactoryA, 1));
  55. Assert.True(SmartOpsOperationalFactory.Matches(TenantA, FactoryA, FactoryA));
  56. Assert.True(SmartOpsOperationalFactory.Matches(TenantA, FactoryA, 0));
  57. Assert.True(SmartOpsOperationalFactory.Matches(TenantA, FactoryA, null));
  58. Assert.False(SmartOpsOperationalFactory.Matches(TenantA, FactoryA, TenantA));
  59. Assert.False(SmartOpsOperationalFactory.Matches(TenantA, FactoryA, FactoryB));
  60. }
  61. [Fact]
  62. public void PlanNo_IsTenantScopedAndMonotonic()
  63. {
  64. var prefix = SmartOpsImprovementPlanNo.BuildPrefix(new DateTime(2026, 8, 17));
  65. Assert.Equal("SOP-20260817-", prefix);
  66. Assert.Equal("SOP-20260817-001", SmartOpsImprovementPlanNo.Next(prefix, Array.Empty<string>()));
  67. Assert.Equal(
  68. "SOP-20260817-003",
  69. SmartOpsImprovementPlanNo.Next(prefix, new[] { "SOP-20260817-001", "SOP-20260817-002" }));
  70. }
  71. [Fact]
  72. public void ConcurrentPlanNo_DoesNotReuseExistingNumbers()
  73. {
  74. var prefix = "SOP-20260817-";
  75. var existing = new List<string>();
  76. var generated = new HashSet<string>();
  77. for (var i = 0; i < 20; i++)
  78. {
  79. var next = SmartOpsImprovementPlanNo.Next(prefix, existing);
  80. Assert.True(generated.Add(next));
  81. existing.Add(next);
  82. }
  83. Assert.Equal(20, generated.Count);
  84. Assert.Contains("SOP-20260817-020", generated);
  85. }
  86. [Fact]
  87. public void DiagnosisAndImprovement_UseTrustedScopeNotRequestTenant()
  88. {
  89. var diagnosis = typeof(AidopKanbanController).GetMethod(nameof(AidopKanbanController.GetSmartDiagnosis));
  90. Assert.NotNull(diagnosis);
  91. var controllerSource = ReadSiblingSource(
  92. typeof(AdoSmartOpsImprovementPlanController),
  93. "AdoSmartOpsImprovementPlanController.cs");
  94. var diagnosisSource = ReadSiblingSource(
  95. typeof(AidopKanbanController),
  96. "AidopKanbanController.Diagnosis.cs");
  97. Assert.DoesNotContain("AidopTenantHelper.GetTenantId", controllerSource);
  98. Assert.DoesNotContain("AidopTenantHelper.GetTenantId", diagnosisSource);
  99. Assert.Contains("_trustedScope.ResolveAsync", controllerSource);
  100. Assert.Contains("_trustedScope.ResolveAsync", diagnosisSource);
  101. Assert.Contains("x.TenantId == plan.TenantId", controllerSource);
  102. Assert.Contains("x.TenantId == tenantId", controllerSource);
  103. Assert.DoesNotContain("[AllowAnonymous]", controllerSource);
  104. Assert.DoesNotContain("FactoryId = input.FactoryId", controllerSource);
  105. }
  106. [Fact]
  107. public void ApproverGuard_ClearsFiltersWhenResolvingRoleMembers()
  108. {
  109. var source = ReadSiblingSource(
  110. typeof(SmartOpsImprovementApproverGuard),
  111. "SmartOpsImprovementApproverGuard.cs");
  112. Assert.Contains("Queryable<SysUserRole>()", source);
  113. Assert.Contains(".ClearFilter()", source);
  114. Assert.Contains("u.TenantId == tenantId", source);
  115. }
  116. [Fact]
  117. public void ApprovalCallback_UpdatesWithTenantCondition()
  118. {
  119. var source = ReadSiblingSource(
  120. typeof(SmartOpsImprovementBizHandler),
  121. "SmartOpsImprovementBizHandler.cs");
  122. Assert.Contains("x.TenantId == plan.TenantId", source);
  123. Assert.DoesNotContain(".Where(x => x.Id == bizId)", source.Replace("x.Id == bizId && x.TenantId == plan.TenantId", ""));
  124. }
  125. private static string ReadSiblingSource(Type type, string fileName)
  126. {
  127. var root = FindRepoRoot();
  128. var matches = Directory.GetFiles(root, fileName, SearchOption.AllDirectories)
  129. .Where(path => path.Replace('\\', '/').Contains("/Admin.NET.Plugin.AiDOP/")
  130. && !path.Contains(".Tests"))
  131. .ToList();
  132. Assert.NotEmpty(matches);
  133. return File.ReadAllText(matches[0]);
  134. }
  135. private static string FindRepoRoot()
  136. {
  137. var dir = new DirectoryInfo(AppContext.BaseDirectory);
  138. while (dir != null)
  139. {
  140. if (File.Exists(Path.Combine(dir.FullName, "AGENTS.md")))
  141. return dir.FullName;
  142. dir = dir.Parent;
  143. }
  144. return Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", ".."));
  145. }
  146. }