|
|
@@ -81,7 +81,16 @@ public class S8TenantIsolationContractTests
|
|
|
[InlineData(typeof(S8WatchRuleService), typeof(AdoS8WatchRule))]
|
|
|
public void ConfigWrites_RequireTrustedScope(Type serviceType, Type entityType)
|
|
|
{
|
|
|
- Assert.NotNull(serviceType.GetMethod("CreateAsync", [entityType, typeof(S8TrustedScope)]));
|
|
|
+ // CreateAsync 用「前两个形参匹配」而非精确签名匹配:
|
|
|
+ // S8-LEGACY-SQL-RESIDUAL-CLEANUP-3 给 S8WatchRuleService.CreateAsync 追加了
|
|
|
+ // 可选形参 origin(string origin = S8RuleCreationOrigin.ExternalApi),
|
|
|
+ // 精确 2 参的 GetMethod 会返回 null,导致本用例误报。
|
|
|
+ // 真正要保证的契约是「entity + S8TrustedScope 必须显式出现在最前」,
|
|
|
+ // 后置可选形参不破坏该契约,故放宽为前缀匹配。
|
|
|
+ Assert.True(
|
|
|
+ HasScopedWriteEntry(serviceType, "CreateAsync", entityType),
|
|
|
+ $"{serviceType.Name}.CreateAsync 必须以 ({entityType.Name}, S8TrustedScope) 开头");
|
|
|
+
|
|
|
Assert.NotNull(serviceType.GetMethod("UpdateAsync", [typeof(long), entityType, typeof(S8TrustedScope)]));
|
|
|
Assert.NotNull(serviceType.GetMethod("DeleteAsync", [typeof(long), typeof(S8TrustedScope)]));
|
|
|
|
|
|
@@ -91,6 +100,22 @@ public class S8TenantIsolationContractTests
|
|
|
Assert.Null(serviceType.GetMethod("DeleteAsync", [typeof(long)]));
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// 写入口是否以 (entity, S8TrustedScope) 开头。允许其后存在形参(如可选的 origin),
|
|
|
+ /// 但作用域必须是第 2 个形参且不可省略——调用方无法只传实体就完成写入。
|
|
|
+ /// </summary>
|
|
|
+ private static bool HasScopedWriteEntry(Type serviceType, string methodName, Type entityType) =>
|
|
|
+ serviceType.GetMethods(BindingFlags.Instance | BindingFlags.Public)
|
|
|
+ .Where(m => m.Name == methodName)
|
|
|
+ .Any(m =>
|
|
|
+ {
|
|
|
+ var p = m.GetParameters();
|
|
|
+ return p.Length >= 2
|
|
|
+ && p[0].ParameterType == entityType
|
|
|
+ && p[1].ParameterType == typeof(S8TrustedScope)
|
|
|
+ && !p[1].IsOptional;
|
|
|
+ });
|
|
|
+
|
|
|
/// <summary>监视规则的每个按 Id 的运行态动作都必须带可信作用域。</summary>
|
|
|
[Theory]
|
|
|
[InlineData("RunNowAsync")]
|