TenantLocationScopeTests.cs 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. using Admin.NET.Plugin.AiDOP.MaterialWarehouse;
  2. using Xunit;
  3. namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
  4. /// <summary>
  5. /// 租户库位安全边界纯逻辑测试(脱库)。
  6. /// 覆盖:白名单归一化、用户筛选只能收窄、越界筛选 fail closed、空边界不得生成 IN、参数化与参数上限。
  7. /// </summary>
  8. public class TenantLocationScopeTests
  9. {
  10. private static TenantLocationScope Uat() =>
  11. TenantLocationScope.FromWhitelist(new[] { "1000", "1001", "1002", "5007", "8001" });
  12. // —— 归一化 ——
  13. [Fact]
  14. public void FromWhitelist_TrimsAndDedupesAndDropsBlanks()
  15. {
  16. var scope = TenantLocationScope.FromWhitelist(new[] { " 1001 ", "1001", "", " ", null, "1002" });
  17. Assert.Equal(new[] { "1001", "1002" }, scope.Locations);
  18. }
  19. [Fact]
  20. public void FromWhitelist_NullOrAllBlank_IsEmpty()
  21. {
  22. Assert.True(TenantLocationScope.FromWhitelist(null).IsEmpty);
  23. Assert.True(TenantLocationScope.FromWhitelist(new string[] { null, "", " " }).IsEmpty);
  24. }
  25. // —— Case 12 / 第八节:用户 Location 筛选不得绕过租户边界 ——
  26. [Fact]
  27. public void Intersect_NoRequestedLocation_KeepsFullTenantScope()
  28. {
  29. var scope = Uat();
  30. Assert.Equal(scope.Locations, scope.Intersect(null).Locations);
  31. Assert.Equal(scope.Locations, scope.Intersect(" ").Locations);
  32. }
  33. [Fact]
  34. public void Intersect_LegalLocation_NarrowsToThatLocation()
  35. {
  36. var narrowed = Uat().Intersect(" 1001 ");
  37. Assert.Single(narrowed.Locations);
  38. Assert.Equal("1001", narrowed.Locations[0]);
  39. }
  40. [Theory]
  41. [InlineData("10000047")]
  42. [InlineData("10000054")]
  43. [InlineData("10000092")]
  44. public void Intersect_ForeignTenantLocation_FailsClosed(string foreign)
  45. {
  46. // Case 5 / Case 12:显式指定他租户库位必须归零,绝不放行
  47. Assert.True(Uat().Intersect(foreign).IsEmpty);
  48. }
  49. [Fact]
  50. public void Intersect_NeverWidensScope()
  51. {
  52. var scope = Uat();
  53. foreach (var candidate in new[] { "1001", "10000047", null, "", "9999" })
  54. {
  55. var result = scope.Intersect(candidate);
  56. Assert.True(result.Count <= scope.Count);
  57. Assert.All(result.Locations, x => Assert.Contains(x, scope.Locations));
  58. }
  59. }
  60. [Fact]
  61. public void Contains_IsCaseInsensitiveAndTrimmed()
  62. {
  63. var scope = TenantLocationScope.FromWhitelist(new[] { "VMI01" });
  64. Assert.True(scope.Contains(" vmi01 "));
  65. Assert.False(scope.Contains("VMI02"));
  66. Assert.False(scope.Contains(null));
  67. }
  68. // —— Case 9 / 第十节:EMPTY SCOPE != FULL DOMAIN ——
  69. [Fact]
  70. public void BuildInClause_EmptyScope_Throws_NeverDegradesToFullDomain()
  71. {
  72. var ex = Assert.Throws<InvalidOperationException>(
  73. () => TenantLocationScope.Empty.BuildInClause("d.Location", "loc"));
  74. Assert.Contains("fail closed", ex.Message);
  75. }
  76. // —— 第十七节:参数化 ——
  77. [Fact]
  78. public void BuildInClause_ParameterizesEveryLocation()
  79. {
  80. var (clause, pars) = Uat().BuildInClause("d.Location", "loc");
  81. Assert.Equal("d.Location IN (@loc0,@loc1,@loc2,@loc3,@loc4)", clause);
  82. Assert.Equal(5, pars.Count);
  83. Assert.Equal(new[] { "1000", "1001", "1002", "5007", "8001" }, pars.Select(p => (string)p.Value));
  84. }
  85. [Fact]
  86. public void BuildInClause_NeverInlinesLocationValueIntoSql()
  87. {
  88. // 库位值一律走参数,SQL 文本里不得出现任何库位字面量(防注入 / 防拼接)
  89. var (clause, _) = TenantLocationScope
  90. .FromWhitelist(new[] { "1001", "O'BRIEN", "x') OR 1=1 --" })
  91. .BuildInClause("d.Location", "loc");
  92. Assert.DoesNotContain("1001", clause);
  93. Assert.DoesNotContain("OR 1=1", clause);
  94. Assert.DoesNotContain("'", clause);
  95. }
  96. [Fact]
  97. public void BuildInClause_RejectsOversizedWhitelist_ToStayUnderDbParameterLimit()
  98. {
  99. var oversized = TenantLocationScope.FromWhitelist(
  100. Enumerable.Range(0, TenantLocationScope.MaxParameters + 1).Select(i => $"L{i}"));
  101. var ex = Assert.Throws<InvalidOperationException>(() => oversized.BuildInClause("d.Location", "loc"));
  102. Assert.Contains("参数上限", ex.Message);
  103. }
  104. [Fact]
  105. public void BuildInClause_AtParameterLimit_IsAccepted()
  106. {
  107. var atLimit = TenantLocationScope.FromWhitelist(
  108. Enumerable.Range(0, TenantLocationScope.MaxParameters).Select(i => $"L{i}"));
  109. var (_, pars) = atLimit.BuildInClause("d.Location", "loc");
  110. Assert.Equal(TenantLocationScope.MaxParameters, pars.Count);
  111. }
  112. }