| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- using Admin.NET.Plugin.AiDOP.MaterialWarehouse;
- using Xunit;
- namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
- /// <summary>
- /// 租户库位安全边界纯逻辑测试(脱库)。
- /// 覆盖:白名单归一化、用户筛选只能收窄、越界筛选 fail closed、空边界不得生成 IN、参数化与参数上限。
- /// </summary>
- public class TenantLocationScopeTests
- {
- private static TenantLocationScope Uat() =>
- TenantLocationScope.FromWhitelist(new[] { "1000", "1001", "1002", "5007", "8001" });
- // —— 归一化 ——
- [Fact]
- public void FromWhitelist_TrimsAndDedupesAndDropsBlanks()
- {
- var scope = TenantLocationScope.FromWhitelist(new[] { " 1001 ", "1001", "", " ", null, "1002" });
- Assert.Equal(new[] { "1001", "1002" }, scope.Locations);
- }
- [Fact]
- public void FromWhitelist_NullOrAllBlank_IsEmpty()
- {
- Assert.True(TenantLocationScope.FromWhitelist(null).IsEmpty);
- Assert.True(TenantLocationScope.FromWhitelist(new string[] { null, "", " " }).IsEmpty);
- }
- // —— Case 12 / 第八节:用户 Location 筛选不得绕过租户边界 ——
- [Fact]
- public void Intersect_NoRequestedLocation_KeepsFullTenantScope()
- {
- var scope = Uat();
- Assert.Equal(scope.Locations, scope.Intersect(null).Locations);
- Assert.Equal(scope.Locations, scope.Intersect(" ").Locations);
- }
- [Fact]
- public void Intersect_LegalLocation_NarrowsToThatLocation()
- {
- var narrowed = Uat().Intersect(" 1001 ");
- Assert.Single(narrowed.Locations);
- Assert.Equal("1001", narrowed.Locations[0]);
- }
- [Theory]
- [InlineData("10000047")]
- [InlineData("10000054")]
- [InlineData("10000092")]
- public void Intersect_ForeignTenantLocation_FailsClosed(string foreign)
- {
- // Case 5 / Case 12:显式指定他租户库位必须归零,绝不放行
- Assert.True(Uat().Intersect(foreign).IsEmpty);
- }
- [Fact]
- public void Intersect_NeverWidensScope()
- {
- var scope = Uat();
- foreach (var candidate in new[] { "1001", "10000047", null, "", "9999" })
- {
- var result = scope.Intersect(candidate);
- Assert.True(result.Count <= scope.Count);
- Assert.All(result.Locations, x => Assert.Contains(x, scope.Locations));
- }
- }
- [Fact]
- public void Contains_IsCaseInsensitiveAndTrimmed()
- {
- var scope = TenantLocationScope.FromWhitelist(new[] { "VMI01" });
- Assert.True(scope.Contains(" vmi01 "));
- Assert.False(scope.Contains("VMI02"));
- Assert.False(scope.Contains(null));
- }
- // —— Case 9 / 第十节:EMPTY SCOPE != FULL DOMAIN ——
- [Fact]
- public void BuildInClause_EmptyScope_Throws_NeverDegradesToFullDomain()
- {
- var ex = Assert.Throws<InvalidOperationException>(
- () => TenantLocationScope.Empty.BuildInClause("d.Location", "loc"));
- Assert.Contains("fail closed", ex.Message);
- }
- // —— 第十七节:参数化 ——
- [Fact]
- public void BuildInClause_ParameterizesEveryLocation()
- {
- var (clause, pars) = Uat().BuildInClause("d.Location", "loc");
- Assert.Equal("d.Location IN (@loc0,@loc1,@loc2,@loc3,@loc4)", clause);
- Assert.Equal(5, pars.Count);
- Assert.Equal(new[] { "1000", "1001", "1002", "5007", "8001" }, pars.Select(p => (string)p.Value));
- }
- [Fact]
- public void BuildInClause_NeverInlinesLocationValueIntoSql()
- {
- // 库位值一律走参数,SQL 文本里不得出现任何库位字面量(防注入 / 防拼接)
- var (clause, _) = TenantLocationScope
- .FromWhitelist(new[] { "1001", "O'BRIEN", "x') OR 1=1 --" })
- .BuildInClause("d.Location", "loc");
- Assert.DoesNotContain("1001", clause);
- Assert.DoesNotContain("OR 1=1", clause);
- Assert.DoesNotContain("'", clause);
- }
- [Fact]
- public void BuildInClause_RejectsOversizedWhitelist_ToStayUnderDbParameterLimit()
- {
- var oversized = TenantLocationScope.FromWhitelist(
- Enumerable.Range(0, TenantLocationScope.MaxParameters + 1).Select(i => $"L{i}"));
- var ex = Assert.Throws<InvalidOperationException>(() => oversized.BuildInClause("d.Location", "loc"));
- Assert.Contains("参数上限", ex.Message);
- }
- [Fact]
- public void BuildInClause_AtParameterLimit_IsAccepted()
- {
- var atLimit = TenantLocationScope.FromWhitelist(
- Enumerable.Range(0, TenantLocationScope.MaxParameters).Select(i => $"L{i}"));
- var (_, pars) = atLimit.BuildInClause("d.Location", "loc");
- Assert.Equal(TenantLocationScope.MaxParameters, pars.Count);
- }
- }
|