|
@@ -0,0 +1,325 @@
|
|
|
|
|
+using System.Text.RegularExpressions;
|
|
|
|
|
+using SqlSugar;
|
|
|
|
|
+using Xunit;
|
|
|
|
|
+
|
|
|
|
|
+namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
|
|
|
|
|
+
|
|
|
|
|
+/// <summary>
|
|
|
|
|
+/// S5 MissedPrint 剩余消费点 P1/P2 收口守卫(FUNC-S5-009 / IqcWmsResultPush / InventoryBalanceReader)。
|
|
|
|
|
+/// <para>
|
|
|
|
|
+/// 源码断言部分无门禁,普通 CI 也会执行;数据面断言需 AIDOP_IT=1,且**全程只读**,
|
|
|
|
|
+/// 不写任何业务表、不触发 165 推送。
|
|
|
|
|
+/// </para>
|
|
|
|
|
+/// <para>
|
|
|
|
|
+/// 背景:MissedPrint.tenant_id 已是正式租户列(生成标签写入 + 1.0.442 回填,152 行 0 NULL)。
|
|
|
|
|
+/// 但 797 的 104 行 Domain='8010'、UAT 的 48 行 Domain='',而 SourceDomainTenantResolver
|
|
|
|
|
+/// 对两者都解析出 '8010' —— 因此凡是按 Domain 过滤 MissedPrint 的路径,对 UAT 而言候选宇宙
|
|
|
|
|
+/// 100% 由 797 构成,方向是反的。本组把「一律直接用 tenant_id」钉死。
|
|
|
|
|
+/// </para>
|
|
|
|
|
+/// </summary>
|
|
|
|
|
+public class MissedPrintConsumerScopeTests
|
|
|
|
|
+{
|
|
|
|
|
+ private const long TenantUat = 838257186181189L;
|
|
|
|
|
+ private const long TenantAidop = 797403760988229L;
|
|
|
|
|
+
|
|
|
|
|
+ private static bool ItEnabled => Environment.GetEnvironmentVariable("AIDOP_IT") == "1";
|
|
|
|
|
+
|
|
|
|
|
+ private readonly Xunit.Abstractions.ITestOutputHelper _out;
|
|
|
|
|
+ public MissedPrintConsumerScopeTests(Xunit.Abstractions.ITestOutputHelper output) => _out = output;
|
|
|
|
|
+
|
|
|
|
|
+ private static readonly string PluginRoot =
|
|
|
|
|
+ Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../../Admin.NET.Plugin.AiDOP"));
|
|
|
|
|
+
|
|
|
|
|
+ private const string PendingSrc = "MaterialWarehouse/PendingInspectionService.cs";
|
|
|
|
|
+ private const string IqcPushSrc = "MaterialWarehouse/IqcWmsResultPushService.cs";
|
|
|
|
|
+ private const string BalanceSrc = "MaterialWarehouse/InventoryBalanceReader.cs";
|
|
|
|
|
+
|
|
|
|
|
+ private static string ReadSource(string relativePath)
|
|
|
|
|
+ {
|
|
|
|
|
+ var full = Path.Combine(PluginRoot, relativePath);
|
|
|
|
|
+ Assert.True(File.Exists(full), $"源码文件不存在,路径需同步更新:{full}");
|
|
|
|
|
+ return File.ReadAllText(full);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>只取可执行代码行:注释里为留档会提到旧写法,不应算违规。</summary>
|
|
|
|
|
+ private static List<string> CodeLines(string relativePath) =>
|
|
|
|
|
+ ReadSource(relativePath)
|
|
|
|
|
+ .Split('\n')
|
|
|
|
|
+ .Select(l => l.Trim())
|
|
|
|
|
+ .Where(l => !l.StartsWith("///", StringComparison.Ordinal)
|
|
|
|
|
+ && !l.StartsWith("//", StringComparison.Ordinal)
|
|
|
|
|
+ && !l.StartsWith("--", StringComparison.Ordinal))
|
|
|
|
|
+ .ToList();
|
|
|
|
|
+
|
|
|
|
|
+ private static ISqlSugarClient? BuildAidopdev()
|
|
|
|
|
+ {
|
|
|
|
|
+ const string path = "/home/yy968/work/New9S/AiDOPWarehouse/server/Admin.NET.Application/Configuration/Database.json";
|
|
|
|
|
+ if (!File.Exists(path)) return null;
|
|
|
|
|
+ var cs = File.ReadLines(path)
|
|
|
|
|
+ .Select(l => l.Trim())
|
|
|
|
|
+ .Where(l => !l.StartsWith("//") && l.Contains("\"ConnectionString\"")
|
|
|
|
|
+ && l.Contains("Database=aidopdev") && l.Contains("123.60.180.165"))
|
|
|
|
|
+ .Select(l => Regex.Match(l, "\"ConnectionString\"\\s*:\\s*\"([^\"]+)\"").Groups[1].Value)
|
|
|
|
|
+ .FirstOrDefault(v => !string.IsNullOrEmpty(v));
|
|
|
|
|
+ if (string.IsNullOrEmpty(cs)) return null;
|
|
|
|
|
+ return new SqlSugarScope(new ConnectionConfig
|
|
|
|
|
+ {
|
|
|
|
|
+ ConfigId = "aidopdev-mp-consumer-it", DbType = DbType.MySql, ConnectionString = cs, IsAutoCloseConnection = true,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>取源码中某条 SQL 片段。</summary>
|
|
|
|
|
+ private static string Slice(string src, string from, string to)
|
|
|
|
|
+ {
|
|
|
|
|
+ var i = src.IndexOf(from, StringComparison.Ordinal);
|
|
|
|
|
+ Assert.True(i >= 0, $"源码中找不到锚点:{from}");
|
|
|
|
|
+ var j = src.IndexOf(to, i + from.Length, StringComparison.Ordinal);
|
|
|
|
|
+ Assert.True(j >= 0, $"源码中找不到结束锚点:{to}");
|
|
|
|
|
+ return src.Substring(i, j - i);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ===================== FUNC-S5-009 PendingInspection =====================
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void F009_Anchor_MustFilterByTenantId()
|
|
|
|
|
+ {
|
|
|
|
|
+ var src = ReadSource(PendingSrc);
|
|
|
|
|
+ Assert.Contains("\"m.tenant_id = @TenantId\"", src, StringComparison.Ordinal);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 锚表不得再带 Domain:它对租户内零过滤价值,却会把整个 UAT 清零。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void F009_Anchor_MustNotFilterByDomain()
|
|
|
|
|
+ {
|
|
|
|
|
+ var offenders = CodeLines(PendingSrc)
|
|
|
|
|
+ .Where(l => l.Contains("m.Domain", StringComparison.Ordinal)
|
|
|
|
|
+ || l.Contains("\"@Domain\"", StringComparison.Ordinal))
|
|
|
|
|
+ .ToList();
|
|
|
|
|
+ Assert.True(offenders.Count == 0, $"009 仍在按 Domain 过滤:{string.Join(" | ", offenders)}");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void F009_SupplierOptions_MustBeTenantScoped()
|
|
|
|
|
+ {
|
|
|
|
|
+ var src = ReadSource(PendingSrc);
|
|
|
|
|
+ var stmt = Slice(src, "SELECT DISTINCT m.Supply", "ORDER BY m.Supply");
|
|
|
|
|
+ Assert.Contains("m.tenant_id = @TenantId", stmt, StringComparison.Ordinal);
|
|
|
|
|
+ Assert.Contains("s.tenant_id = @TenantId", stmt, StringComparison.Ordinal);
|
|
|
|
|
+ Assert.DoesNotContain("@Domain", stmt, StringComparison.Ordinal);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>主数据关联改按租户:(Domain, ItemNum) / (Domain, Supp) 都不唯一,会一对多放大。</summary>
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void F009_MasterDataJoins_MustBeTenantScoped()
|
|
|
|
|
+ {
|
|
|
|
|
+ var src = ReadSource(PendingSrc);
|
|
|
|
|
+ Assert.Contains("LEFT JOIN ItemMaster i ON i.tenant_id = @TenantId", src, StringComparison.Ordinal);
|
|
|
|
|
+ Assert.Contains("LEFT JOIN SuppMaster s ON s.tenant_id = @TenantId", src, StringComparison.Ordinal);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ===================== IqcWmsResultPushService =====================
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void IqcPush_MissedPrint_MustBeTenantScoped()
|
|
|
|
|
+ {
|
|
|
|
|
+ var src = ReadSource(IqcPushSrc);
|
|
|
|
|
+ var stmt = Slice(src, "FROM MissedPrint", "IFNULL(Type,'') <> 'Card'");
|
|
|
|
|
+ Assert.Contains("tenant_id = @TenantId", stmt, StringComparison.Ordinal);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// <c>IFNULL(Domain,'') IN ('', @Domain)</c> 会同时命中 UAT(Domain='')与 797(Domain='8010')
|
|
|
|
|
+ /// 两个分区,是本仓唯一一处结构性合并两租户的 MissedPrint 查询,必须彻底消失。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void IqcPush_MustNotUseCrossPartitionDomainPredicate()
|
|
|
|
|
+ {
|
|
|
|
|
+ var offenders = CodeLines(IqcPushSrc)
|
|
|
|
|
+ .Where(l => l.Contains("IN ('', @Domain)", StringComparison.Ordinal))
|
|
|
|
|
+ .ToList();
|
|
|
|
|
+ Assert.True(offenders.Count == 0, $"仍存在跨分区 Domain 谓词:{string.Join(" | ", offenders)}");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void IqcPush_MustNotHardcodeDomain()
|
|
|
|
|
+ {
|
|
|
|
|
+ var offenders = CodeLines(IqcPushSrc)
|
|
|
|
|
+ .Where(l => l.Contains("\"8010\"", StringComparison.Ordinal))
|
|
|
|
|
+ .ToList();
|
|
|
|
|
+ Assert.True(offenders.Count == 0, $"仍硬编码 Domain:{string.Join(" | ", offenders)}");
|
|
|
|
|
+ // 必须改走统一 resolver 并按当前租户解析
|
|
|
|
|
+ Assert.Contains(CodeLines(IqcPushSrc),
|
|
|
|
|
+ l => l.Contains("_domainTenant.ResolveDomainAsync(SourceCode, tenantId", StringComparison.Ordinal));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>推送链租户一致性:入队的 TenantId 必须就是取数用的 tenantId,不得另起来源。</summary>
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void IqcPush_OutboxTenant_MatchesSourceTenant()
|
|
|
|
|
+ {
|
|
|
|
|
+ var lines = CodeLines(IqcPushSrc);
|
|
|
|
|
+ var outboxTenants = lines.Where(l => l.Contains("TenantId =", StringComparison.Ordinal)).ToList();
|
|
|
|
|
+ Assert.NotEmpty(outboxTenants);
|
|
|
|
|
+ Assert.All(outboxTenants, l => Assert.Contains("TenantId = tenantId", l, StringComparison.Ordinal));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ===================== InventoryBalanceReader(010 富化) =====================
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>Live165 与 STD 两处标签富化都必须按租户,且不得退回 Domain-only。</summary>
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void Balance_BothEnrichments_MustBeTenantScoped()
|
|
|
|
|
+ {
|
|
|
|
|
+ var src = ReadSource(BalanceSrc);
|
|
|
|
|
+ var occurrences = Regex.Matches(src, @"FROM MissedPrint\s*\r?\n\s*WHERE\s+tenant_id=@TenantId").Count;
|
|
|
|
|
+ Assert.Equal(2, occurrences);
|
|
|
|
|
+
|
|
|
|
|
+ var offenders = CodeLines(BalanceSrc)
|
|
|
|
|
+ .Where(l => l.Contains("WHERE Domain=@Domain AND IFNULL(Status", StringComparison.Ordinal))
|
|
|
|
|
+ .ToList();
|
|
|
|
|
+ Assert.True(offenders.Count == 0, $"仍存在 Domain-only 的标签富化:{string.Join(" | ", offenders)}");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>富化只能影响展示列,绝不能碰数量列 —— 这是 010 保持 CLOSED 的前提。</summary>
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public void Balance_Enrichment_TouchesOnlyDisplayColumns()
|
|
|
|
|
+ {
|
|
|
|
|
+ var src = ReadSource(BalanceSrc);
|
|
|
|
|
+ foreach (var stmt in new[]
|
|
|
|
|
+ {
|
|
|
|
|
+ Slice(src, "SELECT Location, ItemNum, IFNULL(LotSerial,'') AS LotSerial", "GROUP BY Location"),
|
|
|
|
|
+ })
|
|
|
|
|
+ {
|
|
|
|
|
+ Assert.Contains("MAX(Shelf)", stmt, StringComparison.Ordinal);
|
|
|
|
|
+ Assert.Contains("MAX(LevelChar)", stmt, StringComparison.Ordinal);
|
|
|
|
|
+ Assert.Contains("MAX(Supply)", stmt, StringComparison.Ordinal);
|
|
|
|
|
+ Assert.DoesNotContain("QtyOnHand", stmt, StringComparison.Ordinal);
|
|
|
|
|
+ Assert.DoesNotContain("AvailStatusQty", stmt, StringComparison.Ordinal);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ===================== 数据面(AIDOP_IT=1,只读) =====================
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// §3.2 的判据固化:Domain 在 MissedPrint 层对租户内零过滤价值(每租户单一 Domain 值),
|
|
|
|
|
+ /// 却会整租户清零 —— UAT 就是被清零的那一个。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public async Task Data_DomainIsUselessAndHarmfulOnMissedPrint()
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!ItEnabled) return;
|
|
|
|
|
+ var db = BuildAidopdev();
|
|
|
|
|
+ Assert.NotNull(db);
|
|
|
|
|
+
|
|
|
|
|
+ var multiDomainTenants = await db!.Ado.GetIntAsync(
|
|
|
|
|
+ "SELECT COUNT(1) FROM (SELECT tenant_id FROM MissedPrint GROUP BY tenant_id HAVING COUNT(DISTINCT IFNULL(Domain,''))>1) x");
|
|
|
|
|
+ var uatUnderDomain8010 = await db.Ado.GetIntAsync(
|
|
|
|
|
+ "SELECT COUNT(1) FROM MissedPrint WHERE tenant_id=@T AND Domain='8010'",
|
|
|
|
|
+ new List<SugarParameter> { new("@T", TenantUat) });
|
|
|
|
|
+ var uatTotal = await db.Ado.GetIntAsync(
|
|
|
|
|
+ "SELECT COUNT(1) FROM MissedPrint WHERE tenant_id=@T", new List<SugarParameter> { new("@T", TenantUat) });
|
|
|
|
|
+
|
|
|
|
|
+ _out.WriteLine($"多 Domain 租户数={multiDomainTenants};UAT 总数={uatTotal},其中 Domain='8010' 的={uatUnderDomain8010}");
|
|
|
|
|
+ Assert.Equal(0, multiDomainTenants); // 每租户单一 Domain → Domain 无租户内过滤价值
|
|
|
|
|
+ Assert.True(uatTotal > 0);
|
|
|
|
|
+ Assert.Equal(0, uatUnderDomain8010); // 而 resolver 给 UAT 的正是 '8010' → 旧写法整租户清零
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>§4.5:IQC 推送候选集,修复前跨两租户 130 行,修复后按租户切分且互不重叠。</summary>
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public async Task Data_IqcPushCandidates_SplitByTenant()
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!ItEnabled) return;
|
|
|
|
|
+ var db = BuildAidopdev();
|
|
|
|
|
+ Assert.NotNull(db);
|
|
|
|
|
+
|
|
|
|
|
+ const string tail = "IFNULL(Status,'')<>'C' AND IFNULL(Type,'')<>'Card'";
|
|
|
|
|
+ var legacy = await db!.Ado.GetIntAsync(
|
|
|
|
|
+ $"SELECT COUNT(1) FROM MissedPrint WHERE IFNULL(Domain,'') IN ('', '8010') AND {tail}");
|
|
|
|
|
+ var uat = await db.Ado.GetIntAsync(
|
|
|
|
|
+ $"SELECT COUNT(1) FROM MissedPrint WHERE tenant_id=@T AND {tail}",
|
|
|
|
|
+ new List<SugarParameter> { new("@T", TenantUat) });
|
|
|
|
|
+ var aidop = await db.Ado.GetIntAsync(
|
|
|
|
|
+ $"SELECT COUNT(1) FROM MissedPrint WHERE tenant_id=@T AND {tail}",
|
|
|
|
|
+ new List<SugarParameter> { new("@T", TenantAidop) });
|
|
|
|
|
+
|
|
|
|
|
+ _out.WriteLine($"旧写法候选={legacy}(跨两租户);按租户切分 UAT={uat} / 797={aidop}");
|
|
|
|
|
+ Assert.True(legacy > uat && legacy > aidop, "旧写法应当同时并入两个租户,否则用例失去意义");
|
|
|
|
|
+ Assert.Equal(legacy, uat + aidop); // 不重不漏
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>§5.3:富化 JOIN 键在租户内唯一,加 tenant_id 后不会放大分页。</summary>
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public async Task Data_EnrichmentJoinKey_UniqueWithinTenant()
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!ItEnabled) return;
|
|
|
|
|
+ var db = BuildAidopdev();
|
|
|
|
|
+ Assert.NotNull(db);
|
|
|
|
|
+
|
|
|
|
|
+ var dupWithinTenant = await db!.Ado.GetIntAsync(
|
|
|
|
|
+ """
|
|
|
|
|
+ SELECT COUNT(1) FROM (
|
|
|
|
|
+ SELECT tenant_id, Location, ItemNum, IFNULL(LotSerial,'') l
|
|
|
|
|
+ FROM MissedPrint WHERE IFNULL(Location,'')<>''
|
|
|
|
|
+ GROUP BY tenant_id, Location, ItemNum, l HAVING COUNT(*)>1) x
|
|
|
|
|
+ """);
|
|
|
|
|
+ // 负向对照:不带租户时,(Location,ItemNum,LotSerial) 在 mdp_std_inventory 已跨租户碰撞
|
|
|
|
|
+ var stdCollision = await db.Ado.GetIntAsync(
|
|
|
|
|
+ """
|
|
|
|
|
+ SELECT COUNT(1) FROM (
|
|
|
|
|
+ SELECT location, item_num, IFNULL(lot_serial,'') l FROM mdp_std_inventory
|
|
|
|
|
+ WHERE (location, item_num, IFNULL(lot_serial,'')) IN (
|
|
|
|
|
+ SELECT Location, ItemNum, IFNULL(LotSerial,'') FROM MissedPrint
|
|
|
|
|
+ WHERE IFNULL(Status,'') NOT IN ('U','P','C') AND IFNULL(Location,'')<>'' AND Qty>0)
|
|
|
|
|
+ GROUP BY location, item_num, l HAVING COUNT(DISTINCT tenant_id)>1) x
|
|
|
|
|
+ """);
|
|
|
|
|
+
|
|
|
|
|
+ _out.WriteLine($"MissedPrint 租户内键重复={dupWithinTenant};标签键在 STD 里跨租户碰撞={stdCollision}");
|
|
|
|
|
+ Assert.Equal(0, dupWithinTenant);
|
|
|
|
|
+ Assert.True(stdCollision > 0, "碰撞若已消失,需重新评估本条 HARDEN 的必要性");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>§3.4:主数据关联改按租户后是 1:1;按 Domain 则会放大。</summary>
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public async Task Data_MasterDataJoin_TenantScopedIsOneToOne()
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!ItEnabled) return;
|
|
|
|
|
+ var db = BuildAidopdev();
|
|
|
|
|
+ Assert.NotNull(db);
|
|
|
|
|
+
|
|
|
|
|
+ var itemByTenant = await db!.Ado.GetIntAsync(
|
|
|
|
|
+ "SELECT COUNT(1) FROM (SELECT tenant_id,ItemNum FROM ItemMaster GROUP BY tenant_id,ItemNum HAVING COUNT(*)>1) x");
|
|
|
|
|
+ var itemByDomain = await db.Ado.GetIntAsync(
|
|
|
|
|
+ "SELECT COUNT(1) FROM (SELECT Domain,ItemNum FROM ItemMaster GROUP BY Domain,ItemNum HAVING COUNT(*)>1) x");
|
|
|
|
|
+ var suppByTenant = await db.Ado.GetIntAsync(
|
|
|
|
|
+ "SELECT COUNT(1) FROM (SELECT tenant_id,Supp FROM SuppMaster GROUP BY tenant_id,Supp HAVING COUNT(*)>1) x");
|
|
|
|
|
+ var suppByDomain = await db.Ado.GetIntAsync(
|
|
|
|
|
+ "SELECT COUNT(1) FROM (SELECT Domain,Supp FROM SuppMaster GROUP BY Domain,Supp HAVING COUNT(*)>1) x");
|
|
|
|
|
+
|
|
|
|
|
+ _out.WriteLine($"ItemMaster 重复:按租户={itemByTenant} 按Domain={itemByDomain};" +
|
|
|
|
|
+ $"SuppMaster 重复:按租户={suppByTenant} 按Domain={suppByDomain}");
|
|
|
|
|
+ Assert.Equal(0, itemByTenant);
|
|
|
|
|
+ Assert.Equal(0, suppByTenant);
|
|
|
|
|
+ Assert.True(itemByDomain > 0 && suppByDomain > 0, "Domain-only 若已唯一,本条放大论证需重做");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>回归:MissedPrint 租户归属未被本批破坏。</summary>
|
|
|
|
|
+ [Fact]
|
|
|
|
|
+ public async Task Data_Regression_TenantSplitUnchanged()
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!ItEnabled) return;
|
|
|
|
|
+ var db = BuildAidopdev();
|
|
|
|
|
+ Assert.NotNull(db);
|
|
|
|
|
+
|
|
|
|
|
+ var total = await db!.Ado.GetIntAsync("SELECT COUNT(1) FROM MissedPrint");
|
|
|
|
|
+ var nullTenant = await db.Ado.GetIntAsync("SELECT COUNT(1) FROM MissedPrint WHERE tenant_id IS NULL");
|
|
|
|
|
+ var uat = await db.Ado.GetIntAsync("SELECT COUNT(1) FROM MissedPrint WHERE tenant_id=@T",
|
|
|
|
|
+ new List<SugarParameter> { new("@T", TenantUat) });
|
|
|
|
|
+ var aidop = await db.Ado.GetIntAsync("SELECT COUNT(1) FROM MissedPrint WHERE tenant_id=@T",
|
|
|
|
|
+ new List<SugarParameter> { new("@T", TenantAidop) });
|
|
|
|
|
+
|
|
|
|
|
+ _out.WriteLine($"total={total} null={nullTenant} UAT={uat} 797={aidop}");
|
|
|
|
|
+ Assert.Equal(0, nullTenant);
|
|
|
|
|
+ Assert.Equal(total, uat + aidop);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|