using System.Reflection; using Admin.NET.Core; using SqlSugar; using Xunit; using AF = Admin.NET.Plugin.ApprovalFlow.ApprovalFlow; namespace Admin.NET.Plugin.AiDOP.Tests.ApprovalFlow; /// /// ApprovalFlow.TenantId 的属性契约。 /// 本实体曾用「重新声明同名属性」隐藏 的 TenantId(CS0114)。 /// 隐藏会造成两处静默故障:((ITenantIdFilter)flow).TenantId 落到基类那个永不赋值的字段上(实测读回 null), /// 以及丢掉基类 IsOnlyIgnoreUpdate = true,使整实体 UPDATE 有机会改写租户归属。 /// 两者当时都没有代码路径触发,编译器也只给一条 warning —— 正因为无声,才需要契约测试钉住。 /// public class ApprovalFlowTenantIdContractTests { private static PropertyInfo TenantIdProperty => typeof(AF).GetProperty(nameof(ITenantIdFilter.TenantId))!; /// TenantId 必须是 override,而不是隐藏基类属性。 [Fact] public void TenantId_Overrides_BaseProperty_RatherThanHidingIt() { var getter = TenantIdProperty.GetGetMethod()!; Assert.True(getter.IsVirtual, "TenantId 必须是 virtual/override 链的一环"); // override 的 getter 其 GetBaseDefinition() 指回基类;隐藏(new)则指向自己。 Assert.Equal(typeof(EntityBaseTenantOrgDel), getter.GetBaseDefinition().DeclaringType); } /// 子类、接口、基类三条访问路径必须读到同一个值(含 null)。 [Theory] [InlineData(797403760988229L)] [InlineData(null)] public void TenantId_IsConsistent_AcrossDerived_Interface_And_Base(long? value) { var flow = new AF { TenantId = value }; Assert.Equal(value, flow.TenantId); Assert.Equal(value, ((ITenantIdFilter)flow).TenantId); Assert.Equal(value, ((EntityBaseTenantOrgDel)flow).TenantId); } /// 反向:经接口写入,子类必须读得到——泛型基础设施按 ITenantIdFilter 赋值时不能落空。 [Fact] public void TenantId_WrittenThroughInterface_IsVisibleOnDerived() { var flow = new AF(); ((ITenantIdFilter)flow).TenantId = 797403760988229L; Assert.Equal(797403760988229L, flow.TenantId); } /// NULL 全局流程语义:列必须可空,且 ORM 不得把它推导成 NOT NULL。 [Fact] public void TenantId_StaysNullable_ForGlobalFlowSemantics() { Assert.Equal(typeof(long?), TenantIdProperty.PropertyType); var sugarColumn = TenantIdProperty .GetCustomAttributes(typeof(SugarColumn), inherit: true) .OfType() .SingleOrDefault(); Assert.NotNull(sugarColumn); Assert.True(sugarColumn!.IsNullable, "TenantId IS NULL 表示全局流程,不能推导成 NOT NULL"); } /// 租户归属只在插入时写定:任何整实体 UPDATE 都不得把 TenantId 带进 SET。 [Fact] public void TenantId_IsExcluded_FromWholeEntityUpdate() { var sugarColumn = TenantIdProperty .GetCustomAttributes(typeof(SugarColumn), inherit: true) .OfType() .Single(); Assert.True(sugarColumn.IsOnlyIgnoreUpdate, "IsOnlyIgnoreUpdate 承自基类,丢掉它整实体更新就能改写租户归属"); } /// ORM 只能看到一个 TenantId,不允许父子双属性同时进入列元数据。 [Fact] public void TenantId_MapsToExactlyOneClrProperty() { var properties = typeof(AF) .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy) .Where(p => p.Name == nameof(ITenantIdFilter.TenantId)) .ToList(); Assert.Single(properties); Assert.Equal(typeof(AF), properties[0].DeclaringType); } }