| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- using Xunit;
- namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
- /// <summary>
- /// S5 PURCHASE RECEIPT DISPLAY FIX 源码契约测试。
- ///
- /// 缺陷:<c>JSON_UNQUOTE(JSON_EXTRACT(raw_data,'$.x'))</c> 对 JSON null 产出的是**长度 4 的字面量字符串 'null'**
- /// (<c>IS NULL = 0</c>),一路落进 <c>mdp_std_purchase_receipt</c> 并在采购收货单页面显示成 `null`,
- /// 前端 <c>|| '-'</c> 之类空值兜底对它完全无效。
- ///
- /// 修法:展示/属性列一律经 <c>MdpJsonSql.Str / Int / Dec</c>;
- /// <b>identity / JOIN 键刻意保留裸取值</b>——<c>Domain</c> / <c>Receiver</c> / <c>Line</c> 是
- /// <c>uk_mdp_std_pur_rct</c> 的 NOT NULL 组成部分,且头明细 INNER JOIN 依赖 Domain 相等;
- /// 若一并归一为 SQL NULL,则 <c>NULL = NULL</c> 不成立、整批收货行会从标准层消失。
- /// 本测试同时钉住"该归一的归一"与"不该动的没动"两侧。
- /// </summary>
- public class S5PurchaseReceiptDisplayContractTests
- {
- private static string SyncService() => File.ReadAllText(FindFile(
- "server", "Plugins", "Admin.NET.Plugin.AiDOP", "MaterialWarehouse", "PurchaseReceiptMdpSyncService.cs"));
- private static string JsonSql() => File.ReadAllText(FindFile(
- "server", "Plugins", "Admin.NET.Plugin.AiDOP", "DataPlatform", "MdpJsonSql.cs"));
- /// <summary>标准层的字符串展示列必须走 MdpJsonSql.Str。</summary>
- [Theory]
- [InlineData("p", "Supp")] // supp
- [InlineData("d", "Supp")] // sort_name 的编码部分
- [InlineData("p", "ItemNum")] // item_num
- [InlineData("p", "UM")] // um
- [InlineData("p", "LotSerial")] // lot_serial
- [InlineData("p", "Location")] // location
- [InlineData("p", "OrdNbr")] // ord_nbr
- public void StandardTransform_StringDisplayColumn_UsesNullSafeHelper(string alias, string field)
- {
- Assert.Contains($"MdpJsonSql.Str(\"{alias}\", \"{field}\")", SyncService());
- }
- /// <summary>数值展示列用类型 helper,杜绝 STRICT 模式下的 CAST('null' AS ...)。</summary>
- [Fact]
- public void StandardTransform_NumericDisplayColumns_UseTypedHelpers()
- {
- var s = SyncService();
- Assert.Contains("MdpJsonSql.Dec(\"p\", \"QtyOrded\", 18, 6)", s);
- Assert.Contains("MdpJsonSql.Dec(\"p\", \"QtyReceived\", 18, 6)", s);
- Assert.Contains("MdpJsonSql.Int(\"p\", \"OrdLine\")", s);
- Assert.DoesNotContain("MdpJsonSql.Str(\"p\", \"QtyOrded\")", s);
- Assert.DoesNotContain("MdpJsonSql.Str(\"p\", \"QtyReceived\")", s);
- Assert.DoesNotContain("MdpJsonSql.Str(\"p\", \"OrdLine\")", s);
- }
- /// <summary>修复前的裸写法不得回归(这些正是把 'null' 写进标准层的那几行)。</summary>
- [Theory]
- [InlineData("JSON_UNQUOTE(JSON_EXTRACT(p.raw_data,'$.UM'))")]
- [InlineData("JSON_UNQUOTE(JSON_EXTRACT(p.raw_data,'$.LotSerial'))")]
- [InlineData("JSON_UNQUOTE(JSON_EXTRACT(p.raw_data,'$.Location'))")]
- [InlineData("CAST(JSON_UNQUOTE(JSON_EXTRACT(p.raw_data,'$.QtyOrded')) AS DECIMAL(18,6))")]
- [InlineData("CAST(JSON_UNQUOTE(JSON_EXTRACT(p.raw_data,'$.QtyReceived')) AS DECIMAL(18,6))")]
- public void StandardTransform_BareDisplayExtraction_IsGone(string bareSql)
- {
- Assert.DoesNotContain(bareSql, SyncService());
- }
- /// <summary>
- /// OrdLine 的裸 CAST 只剩 PurOrdDetail 关联条件里的两处(pd / sd),SELECT 列已改用 helper。
- /// 这两处是 JOIN 键,按本批约定不动;数量钉死可挡住"顺手也改了 join"或"漏改 SELECT"两种回归。
- /// </summary>
- [Fact]
- public void OrdLineBareCast_OnlyRemainsInJoinConditions()
- {
- var s = SyncService();
- var bare = s.Split("CAST(JSON_UNQUOTE(JSON_EXTRACT(p.raw_data,'$.OrdLine')) AS SIGNED)").Length - 1;
- Assert.Equal(2, bare);
- Assert.Contains("ELSE CAST(JSON_UNQUOTE(JSON_EXTRACT(p.raw_data,'$.OrdLine')) AS SIGNED) END) = pd.Line", s);
- Assert.Contains("AND CAST(JSON_UNQUOTE(JSON_EXTRACT(p.raw_data,'$.OrdLine')) AS SIGNED) = sd.Line", s);
- }
- /// <summary>
- /// 头明细 INNER JOIN 仍按裸取值比较 Domain / Receiver。
- /// 这是刻意的:改成 MdpJsonSql.Str 会让源侧 Domain 为 NULL 的收货单 <c>NULL = NULL</c> 不成立、整批消失。
- /// </summary>
- [Fact]
- public void MasterDetailJoinKeys_StayBare_SoRowsDoNotVanish()
- {
- var s = SyncService();
- Assert.Contains(
- "JSON_UNQUOTE(JSON_EXTRACT(p.raw_data,'$.Domain')) = JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Domain'))", s);
- Assert.Contains(
- "JSON_UNQUOTE(JSON_EXTRACT(p.raw_data,'$.Receiver')) = JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.Receiver'))", s);
- Assert.DoesNotContain("MdpJsonSql.Str(\"p\", \"Domain\")", s);
- Assert.DoesNotContain("MdpJsonSql.Str(\"p\", \"Receiver\")", s);
- }
- /// <summary>identity 列 domain / receiver / line 与 source_biz_key 兜底保持裸取值(NOT NULL 唯一键成分)。</summary>
- [Fact]
- public void IdentityColumns_StayBare()
- {
- var s = SyncService();
- Assert.Contains("CAST(JSON_UNQUOTE(JSON_EXTRACT(p.raw_data,'$.Line')) AS SIGNED)", s);
- Assert.Contains("IFNULL(NULLIF(p.source_biz_key,''), CONCAT(", s);
- Assert.DoesNotContain("MdpJsonSql.Int(\"p\", \"Line\")", s);
- }
- /// <summary>rct_date 既有的 'null'/空 守卫不得被去掉(本批不重写它,只保证不回归)。</summary>
- [Fact]
- public void RctDate_KeepsExistingNullGuard()
- {
- Assert.Contains(
- "NULLIF(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(d.raw_data,'$.RctDate')),'null'),'')", SyncService());
- }
- /// <summary>RctType='rc' 业务过滤未被本批波及。</summary>
- [Fact]
- public void RctTypeFilter_Unchanged()
- {
- Assert.Contains("JSON_UNQUOTE(JSON_EXTRACT(p.raw_data,'$.RctType'))='rc'", SyncService());
- }
- /// <summary>helper 语义钉死:JSON null / 字符串 'null' → SQL NULL。</summary>
- [Fact]
- public void JsonSqlStrHelper_NormalizesLiteralNullToSqlNull()
- {
- Assert.Contains("NULLIF({Ext(alias, field)},'null')", JsonSql());
- }
- private static string FindFile(params string[] parts)
- {
- var dir = new DirectoryInfo(AppContext.BaseDirectory);
- while (dir != null)
- {
- var candidate = Path.Combine(new[] { dir.FullName }.Concat(parts).ToArray());
- if (File.Exists(candidate)) return candidate;
- dir = dir.Parent;
- }
- throw new FileNotFoundException(string.Join("/", parts));
- }
- }
|