| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542 |
- using System.Text.Json;
- using Admin.NET.Plugin.AiDOP.Service.S7;
- using Xunit;
- namespace Admin.NET.Plugin.AiDOP.Tests.S7;
- /// <summary>
- /// S7-LEGACY-SNAPSHOT-MIGRATION-1 契约测试:S7 存量 Active 实例快照权威迁移。
- ///
- /// 行为级部分直接调用 <see cref="S7LegacyFlowAuthorityMigrationService"/> 的两个纯静态函数
- /// (解析 / 改写),源码契约部分断言门禁与边界仍在。DB 相关的候选选取与事务落库
- /// 由 Runtime dry-run / apply 覆盖(service 依赖 ISqlSugarClient,裸 xUnit 进程无法实例化)。
- /// </summary>
- public class S7LegacyFlowAuthorityMigrationContractTests
- {
- private const string LegacySupervisorId = "1329915020002";
- private const string LegacyQeId = "1329915020003";
- private const string SupervisorCode = "ROLE_S7_FQC_SUPERVISOR";
- private const string QeCode = "ROLE_S7_FQC_QE";
- private static string Service() => File.ReadAllText(FindFile(
- "server", "Plugins", "Admin.NET.Plugin.AiDOP", "Service", "S7", "S7LegacyFlowAuthorityMigrationService.cs"));
- private static string Entity() => File.ReadAllText(FindFile(
- "server", "Plugins", "Admin.NET.Plugin.AiDOP", "Entity", "S7", "AdoS7FlowAuthorityMigrationLog.cs"));
- private static string FlowTaskService() => File.ReadAllText(FindFile(
- "server", "Plugins", "Admin.NET.Plugin.ApprovalFlow", "Service", "FlowTask", "FlowTaskService.cs"));
- private static string Engine() => File.ReadAllText(FindFile(
- "server", "Plugins", "Admin.NET.Plugin.ApprovalFlow", "Service", "FlowEngine", "FlowEngineService.cs"));
- /// <summary>与真实 S7 快照同形:N1=Initiator、N2/N3=Role 数字,另含 SpecificUser / Department 干扰节点。</summary>
- private static string SampleSnapshot(string n2Ids = LegacySupervisorId, string n3Ids = LegacyQeId) => JsonSerializer.Serialize(new
- {
- nodes = new object[]
- {
- new { id = "start", type = "bpmn:startEvent", properties = new { nodeName = "开始" } },
- new { id = "N1_INSPECT", type = "bpmn:userTask", properties = new { nodeName = "检验员检验", approverType = "Initiator", approverIds = "", approverNames = "认领人(发起人)" } },
- new { id = "N2_SUP_REVIEW", type = "bpmn:userTask", properties = new { nodeName = "检验主管审核", approverType = "Role", approverIds = n2Ids, approverNames = "成品检验主管" } },
- new { id = "GW_RESULT", type = "bpmn:exclusiveGateway", properties = new { nodeName = "判定分流" } },
- new { id = "N3_QE_DISPOSITION", type = "bpmn:userTask", properties = new { nodeName = "QE处置", approverType = "Role", approverIds = n3Ids, approverNames = "成品检验QE" } },
- // 干扰项:数字但语义不是 RoleId
- new { id = "X_SPECIFIC", type = "bpmn:userTask", properties = new { nodeName = "指定人", approverType = "SpecificUser", approverIds = "1300000000101", approverNames = "超管" } },
- new { id = "X_DEPT", type = "bpmn:userTask", properties = new { nodeName = "部门", approverType = "Department", approverIds = "1300000000002", approverNames = "市场部" } },
- new { id = "end", type = "bpmn:endEvent", properties = new { nodeName = "结束" } },
- },
- edges = new object[] { new { id = "edge-start-n1", sourceNodeId = "start", targetNodeId = "N1_INSPECT" } },
- });
- // ───────────── §43-1..4 候选谓词 ─────────────
- /// <summary>§43-1:legacy Running 快照(含数字 Role token)应被识别为可迁。</summary>
- [Fact]
- public void Predicate_LegacyRunningSnapshot_IsCandidate()
- {
- var refs = S7LegacyFlowAuthorityMigrationService.ParseNumericRoleRefs(SampleSnapshot());
- Assert.Equal(2, refs.Count);
- Assert.Contains(refs, r => r.NodeId == "N2_SUP_REVIEW" && r.NumericTokens.Single() == LegacySupervisorId);
- Assert.Contains(refs, r => r.NodeId == "N3_QE_DISPOSITION" && r.NumericTokens.Single() == LegacyQeId);
- }
- /// <summary>§43-2:已是 RoleCode 的快照(上一批新建的健康实例)选不出任何数字引用 → 非候选。</summary>
- [Fact]
- public void Predicate_RoleCodeSnapshot_IsNotCandidate()
- {
- var refs = S7LegacyFlowAuthorityMigrationService.ParseNumericRoleRefs(SampleSnapshot(SupervisorCode, QeCode));
- Assert.Empty(refs);
- }
- /// <summary>§43-3:终态实例由 Status==Running 谓词排除;§43-4:BizType 白名单只含 S7。</summary>
- [Fact]
- public void Predicate_RequiresRunningStatusAndS7BizType()
- {
- var body = Slice(Service(), "public async Task<List<S7MigrationCandidate>> SelectCandidatesAsync", "public async Task<S7FlowAuthorityMigrationResult> MigrateAsync");
- Assert.Contains("x.Status == FlowInstanceStatusEnum.Running", body);
- Assert.Contains("bizTypes.Contains(x.BizType)", body);
- Assert.Equal("S7_FQC_INSPBILL", S7LegacyFlowAuthorityMigrationService.TargetBizTypes.Single());
- }
- /// <summary>ACTIVE 判据不能只看 Status —— 必须另要求存在 Pending 任务(S7 有 3 条终态实例仍停在 N3_*)。</summary>
- [Fact]
- public void Predicate_AlsoRequiresPendingTask()
- {
- var body = Slice(Service(), "public async Task<List<S7MigrationCandidate>> SelectCandidatesAsync", "public async Task<S7FlowAuthorityMigrationResult> MigrateAsync");
- Assert.Contains("t.Status == FlowTaskStatusEnum.Pending", body);
- Assert.Contains("if (!hasPending) continue;", body);
- }
- /// <summary>批次边界存在且早于「RoleCode 定义发布」之后的新实例,杜绝退化成长期 auto-heal。</summary>
- [Fact]
- public void Predicate_HasBatchCutoff()
- {
- var s = Service();
- Assert.Contains("BatchCutoff = new(2026, 9, 14, 22, 0, 0", s);
- Assert.Contains("x.StartTime < cutoff", s);
- }
- // ───────────── §43-5..9 改写 ─────────────
- /// <summary>§43-5:N2 数字 RoleId → Supervisor RoleCode。</summary>
- [Fact]
- public void Rewrite_N2_NumericRoleIdBecomesSupervisorCode()
- {
- var after = Rewrite(SampleSnapshot());
- Assert.Equal(SupervisorCode, ApproverIdsOf(after, "N2_SUP_REVIEW"));
- }
- /// <summary>§43-6:N3 数字 RoleId → QE RoleCode。</summary>
- [Fact]
- public void Rewrite_N3_NumericRoleIdBecomesQeCode()
- {
- var after = Rewrite(SampleSnapshot());
- Assert.Equal(QeCode, ApproverIdsOf(after, "N3_QE_DISPOSITION"));
- }
- /// <summary>§43-7 / §13:N1 是 Initiator 节点,approverType 与空 approverIds 必须原样不动。</summary>
- [Fact]
- public void Rewrite_N1_InitiatorIsUntouched()
- {
- var after = Rewrite(SampleSnapshot());
- Assert.Equal("Initiator", ApproverTypeOf(after, "N1_INSPECT"));
- Assert.Equal("", ApproverIdsOf(after, "N1_INSPECT"));
- Assert.DoesNotContain("N1_INSPECT", ChangedNodesOf(SampleSnapshot()));
- }
- /// <summary>§43-8:非 Role 节点里的数字(SpecificUser=UserId / Department=OrgId)不得被改。</summary>
- [Fact]
- public void Rewrite_UnrelatedNumericNodesUntouched()
- {
- var after = Rewrite(SampleSnapshot());
- Assert.Equal("1300000000101", ApproverIdsOf(after, "X_SPECIFIC"));
- Assert.Equal("1300000000002", ApproverIdsOf(after, "X_DEPT"));
- }
- /// <summary>节点 id / 名称 / edges / 网关 / approverNames 一概不动,只有 approverIds 变。</summary>
- [Fact]
- public void Rewrite_PreservesEverythingElse()
- {
- var before = SampleSnapshot();
- var after = Rewrite(before);
- var b = JsonDocument.Parse(before).RootElement;
- var a = JsonDocument.Parse(after).RootElement;
- Assert.Equal(b.GetProperty("nodes").GetArrayLength(), a.GetProperty("nodes").GetArrayLength());
- Assert.Equal(b.GetProperty("edges").ToString(), a.GetProperty("edges").ToString());
- Assert.Equal("成品检验主管", a.GetProperty("nodes").EnumerateArray()
- .First(n => n.GetProperty("id").GetString() == "N2_SUP_REVIEW")
- .GetProperty("properties").GetProperty("approverNames").GetString());
- }
- /// <summary>多 token 时保持原顺序与数量,只替换命中的那个。</summary>
- [Fact]
- public void Rewrite_KeepsTokenOrderAndCount()
- {
- var after = S7LegacyFlowAuthorityMigrationService.RewriteAuthority(
- SampleSnapshot($"999,{LegacySupervisorId},888"),
- new Dictionary<string, string> { [LegacySupervisorId] = SupervisorCode })!;
- Assert.Equal($"999,{SupervisorCode},888", ApproverIdsOf(after.After, "N2_SUP_REVIEW"));
- }
- /// <summary>§43-9 / §12:只能走结构化 JSON,禁止字符串/正则替换。</summary>
- [Theory]
- [InlineData("string.Replace")]
- [InlineData(".Replace(")]
- [InlineData("Regex")]
- public void Rewrite_StructuredJsonOnly(string forbidden)
- {
- Assert.DoesNotContain(forbidden, CsCode(Service()));
- }
- /// <summary>非法 JSON / 无 nodes 一律返回 null,不做任何猜测性改写。</summary>
- [Fact]
- public void Rewrite_FailsClosedOnMalformedSnapshot()
- {
- Assert.Null(S7LegacyFlowAuthorityMigrationService.RewriteAuthority("not json", new Dictionary<string, string>()));
- Assert.Null(S7LegacyFlowAuthorityMigrationService.RewriteAuthority("{\"x\":1}", new Dictionary<string, string>()));
- Assert.Empty(S7LegacyFlowAuthorityMigrationService.ParseNumericRoleRefs("not json"));
- Assert.Empty(S7LegacyFlowAuthorityMigrationService.ParseNumericRoleRefs(null));
- }
- // ───────────── §14 精确路径门禁 ─────────────
- /// <summary>改写结果必须回报被改节点与可读路径,供调用方独立复核。</summary>
- [Fact]
- public void Rewrite_ReportsChangedPaths()
- {
- var r = S7LegacyFlowAuthorityMigrationService.RewriteAuthority(SampleSnapshot(), FullMap())!;
- Assert.Equal(2, r.ChangedNodeIds.Count);
- Assert.Contains("nodes[id=N2_SUP_REVIEW].properties.approverIds", r.ChangedPathText);
- Assert.Contains("nodes[id=N3_QE_DISPOSITION].properties.approverIds", r.ChangedPathText);
- Assert.Contains($"\"{LegacySupervisorId}\" => \"{SupervisorCode}\"", r.ChangedPathText);
- }
- /// <summary>可迁节点集恰为 N2/N3;改写路径越界必须 BLOCKED_CHANGED_PATH。</summary>
- [Fact]
- public void ChangedPathGate_OnlyAllowsN2AndN3()
- {
- Assert.Equal(
- new[] { "N2_SUP_REVIEW", "N3_QE_DISPOSITION" }.OrderBy(x => x),
- S7LegacyFlowAuthorityMigrationService.MigratableNodeIds.OrderBy(x => x));
- var s = Service();
- Assert.Contains("!MigratableNodeIds.Contains(r.NodeId)", s);
- Assert.Contains("BLOCKED_CHANGED_PATH", s);
- Assert.Contains("rewrite.ChangedNodeIds.Where(n => !MigratableNodeIds.Contains(n))", s);
- }
- /// <summary>目标 Code 必须落在 S7 白名单内,防止把意外 Code 写进快照。</summary>
- [Fact]
- public void TargetCodeGate_HasAllowList()
- {
- Assert.Equal(
- new[] { QeCode, SupervisorCode }.OrderBy(x => x),
- S7LegacyFlowAuthorityMigrationService.AllowedTargetRoleCodes.OrderBy(x => x));
- Assert.Contains("!AllowedTargetRoleCodes.Contains(code)", Service());
- }
- // ───────────── §43-10..13 Role 校验 fail-closed ─────────────
- /// <summary>§43-10/11/13:目标租户下该 Code 必须恰好一个启用角色(0 或多个都拒)。</summary>
- [Fact]
- public void RoleGate_RequiresExactlyOneEnabledLocalRole()
- {
- var s = Service();
- Assert.Contains("x.TenantId == c.BusinessTenantId && x.Code == code && x.Status == StatusEnum.Enable", s);
- Assert.Contains("if (targets.Count != 1)", s);
- Assert.Contains("AMBIGUOUS TARGET ROLE", s);
- }
- /// <summary>§43-12:目标角色必须有同租户成员,否则迁完仍解析 0 人 → 拒。</summary>
- [Fact]
- public void RoleGate_RequiresSameTenantMember()
- {
- var s = Service();
- Assert.Contains("u.TenantId == c.BusinessTenantId", s);
- Assert.Contains("if (sameTenantMembers == 0)", s);
- Assert.Contains("无同租户成员", s);
- }
- /// <summary>同租户的数字 RoleId 是合法引用,不得因为「是数字」就改。</summary>
- [Fact]
- public void RoleGate_DoesNotTouchSameTenantNumericRole()
- {
- Assert.Contains("legacyRole.TenantId == c.BusinessTenantId", Service());
- }
- /// <summary>§16:快照只能写 RoleCode,代码里不得出现任何租户本地 RoleId 字面量。</summary>
- [Theory]
- [InlineData("848821564117061")]
- [InlineData("848821564616773")]
- [InlineData("848821570179141")]
- [InlineData("848821570670661")]
- public void NeverHardcodesTenantLocalRoleId(string localRoleId)
- {
- Assert.DoesNotContain(localRoleId, Service());
- }
- /// <summary>§17:业务租户必须由 S7 业务表反查,不得取登录 / 定义 / 发起人 / 待办人租户。</summary>
- [Fact]
- public void BusinessTenant_ComesFromBusinessRow()
- {
- var body = Slice(Service(), "private async Task<long?> ResolveBusinessTenantAsync", "internal static List<S7NumericRoleRef>");
- Assert.Contains("SELECT tenant_id FROM qms_qcpp_inspbill WHERE id=@id", body);
- Assert.DoesNotContain("_userManager", Service());
- }
- // ───────────── §43-14..17 并发 / 事务 / 留证 / 幂等 ─────────────
- /// <summary>§43-14 / §18 / §20:UPDATE 必须带 Id + Running + 改前 MD5,affected≠1 即抛。</summary>
- [Fact]
- public void ConcurrencyGuard_UpdateIsConditional()
- {
- var s = Service();
- Assert.Contains("WHERE Id=@id AND Status=@running AND MD5(FlowJsonSnapshot)=@beforeMd5", s);
- Assert.Contains("if (affected != 1)", s);
- Assert.Contains("CONCURRENT MODIFICATION", s);
- }
- /// <summary>UPDATE 只改 FlowJsonSnapshot 一列 —— 不得用整实体 Updateable 回写过期列。</summary>
- [Fact]
- public void Update_TouchesOnlySnapshotColumn()
- {
- var s = Service();
- Assert.Contains("SET FlowJsonSnapshot=@after", s);
- Assert.DoesNotContain("AsUpdateable(inst)", s);
- Assert.DoesNotContain("Updateable<ApprovalFlowInstance>", s);
- }
- /// <summary>§43-15 / §19:留证 INSERT 与快照 UPDATE 必须同一事务,失败整体回滚。</summary>
- [Fact]
- public void Transaction_EvidenceAndUpdateAreAtomic()
- {
- var body = Slice(Service(), "var tran = await _db.AsTenant().UseTranAsync", "if (!tran.IsSuccess)");
- Assert.Contains("_logRep.AsInsertable", body);
- Assert.Contains("UPDATE ApprovalFlowInstance", body);
- Assert.Contains("if (!tran.IsSuccess)", Service());
- }
- /// <summary>§43-16 / §10:留证表 append-only 且含全部要求字段。</summary>
- [Theory]
- [InlineData("migration_batch")]
- [InlineData("migration_key")]
- [InlineData("instance_id")]
- [InlineData("biz_type")]
- [InlineData("business_tenant_id")]
- [InlineData("before_snapshot")]
- [InlineData("after_snapshot")]
- [InlineData("before_md5")]
- [InlineData("after_md5")]
- [InlineData("changed_paths")]
- [InlineData("rollback_source")]
- [InlineData("applied_by")]
- [InlineData("applied_at")]
- [InlineData("outcome")]
- public void EvidenceTable_HasRequiredColumn(string column)
- {
- Assert.Contains($"ColumnName = \"{column}\"", Entity());
- }
- /// <summary>留证表不得被后续运行抹掉(对照 S8 那张每轮重写的表)。</summary>
- [Fact]
- public void EvidenceTable_IsAppendOnly()
- {
- Assert.DoesNotContain("Deleteable", CsCode(Service()));
- Assert.Contains("append-only", Entity());
- }
- /// <summary>被挡下的候选也必须留证 —— 「为什么没迁」同样要能回答。</summary>
- [Fact]
- public void BlockedCandidates_AlsoWriteEvidence()
- {
- var body = Slice(Service(), "private async Task BlockAsync", "private static string MigrationIdentity");
- Assert.Contains("_logRep.AsInsertable", body);
- Assert.Contains("Outcome = outcome", body);
- }
- /// <summary>§43-17 / §28:幂等判据是快照内容本身,不是留证表里有没有这条实例。</summary>
- [Fact]
- public void Idempotency_DerivesFromSnapshotContentNotEvidenceTable()
- {
- var body = Slice(Service(), "public async Task<List<S7MigrationCandidate>> SelectCandidatesAsync", "public async Task<S7FlowAuthorityMigrationResult> MigrateAsync");
- Assert.Contains("ParseNumericRoleRefs(inst.FlowJsonSnapshot)", body);
- Assert.Contains("if (refs.Count == 0) continue;", body);
- // 候选选取不得查留证表
- Assert.DoesNotContain("_logRep", body);
- }
- /// <summary>§25:dry-run(候选选取)不得写任何表,包括迁移留证表。</summary>
- [Fact]
- public void DryRun_WritesNothing()
- {
- var body = Slice(Service(), "public async Task<List<S7MigrationCandidate>> SelectCandidatesAsync", "public async Task<S7FlowAuthorityMigrationResult> MigrateAsync");
- foreach (var write in new[] { "AsInsertable", "AsUpdateable", "ExecuteCommandAsync", "UseTranAsync", "Deleteable" })
- Assert.DoesNotContain(write, body);
- }
- /// <summary>§11:回滚来源必须唯一且与当前快照逐字节一致。</summary>
- [Fact]
- public void RollbackSource_RequiresExactlyOneMatchingVersionRow()
- {
- var s = Service();
- Assert.Contains("v.FlowId == inst.FlowId && v.Version == inst.FlowVersion", s);
- Assert.Contains("Md5(v.FlowJson ?? string.Empty) == beforeMd5", s);
- Assert.Contains("versionRows.Count != 1 || usable.Count != 1", s);
- Assert.Contains("BLOCKED_ROLLBACK_SOURCE", s);
- }
- // ───────────── §43-18/19 特殊实例 / 新实例 ─────────────
- /// <summary>
- /// §43-18 / §24:迁移不得触碰 Initiator、不得改节点类型、不得重建待办 —— 特殊实例
- /// 840240525086789 的 Initiator tenant mismatch 必须保持 OPEN。
- /// </summary>
- [Theory]
- [InlineData("InitiatorId =")]
- [InlineData("CurrentNodeId =")]
- [InlineData("approverType\"] =")]
- [InlineData("CreateTasksForNode")]
- [InlineData("ReturnToPrev")]
- [InlineData("StartFlow")]
- [InlineData("Approve(")]
- public void Migration_NeverTouchesFlowStateOrInitiator(string forbidden)
- {
- Assert.DoesNotContain(forbidden, CsCode(Service()));
- }
- /// <summary>迁移不得删除或改写任何任务 / 日志 / 已完成节点。</summary>
- [Theory]
- [InlineData("ApprovalFlowTask")]
- [InlineData("ApprovalFlowLog")]
- [InlineData("ApprovalFlowCompletedNode")]
- public void Migration_OnlyReadsFlowSideTables(string table)
- {
- var code = CsCode(Service());
- // 允许只读查询(候选判据要看 Pending),但不允许出现写操作
- foreach (var write in new[] { $"AsUpdateable<{table}>", $"Deleteable<{table}>", $"AsInsertable<{table}>" })
- Assert.DoesNotContain(write, code);
- }
- /// <summary>§43-19:新建的 RoleCode 实例因快照无数字 token 天然落选(已由 Predicate 测试覆盖),此处冻结该推理链。</summary>
- [Fact]
- public void HealthyRoleCodeInstances_ExcludedByContentPredicate()
- {
- Assert.Empty(S7LegacyFlowAuthorityMigrationService.ParseNumericRoleRefs(SampleSnapshot(SupervisorCode, QeCode)));
- Assert.Contains("上一批新建的 RoleCode 实例", Service());
- }
- // ───────────── Dry-run 与 Apply 共用门禁(§25/§26)─────────────
- /// <summary>
- /// dry-run 与 apply 必须走同一个 <c>EvaluateAsync</c>:否则「预演看到的」与「实际改的」
- /// 会是两套逻辑,Dry Run Gate 就失去意义。
- /// </summary>
- [Fact]
- public void DryRunAndApply_ShareTheSameEvaluation()
- {
- var s = Service();
- var dry = Slice(s, "public async Task<List<S7MigrationEvaluation>> DryRunAsync", "private async Task MigrateOneAsync");
- var one = Slice(s, "private async Task MigrateOneAsync", "private async Task<S7MigrationEvaluation> EvaluateAsync");
- Assert.Contains("await EvaluateAsync(c)", dry);
- Assert.Contains("await EvaluateAsync(c)", one);
- // 门禁只实现一份
- Assert.Equal(1, CountOf(s, "private async Task<S7MigrationEvaluation> EvaluateAsync"));
- }
- /// <summary>被挡下的评估由 <c>Blocked(...)</c> 纯构造返回,不在评估阶段写库。</summary>
- [Fact]
- public void Evaluation_BlockedIsPureAndWritesNothing()
- {
- var body = Slice(Service(), "private static S7MigrationEvaluation Blocked(", "private async Task ApplyOneAsync");
- Assert.Contains("CanApply = false", body);
- foreach (var write in new[] { "AsInsertable", "ExecuteCommandAsync", "UseTranAsync", "_logRep" })
- Assert.DoesNotContain(write, body);
- }
- /// <summary>
- /// 一次性执行方式:由 Startup 在启动期调用,先 DryRun 再 Migrate;
- /// 不得引入后台长期轮询任务(§41)。
- /// </summary>
- [Fact]
- public void Execution_IsStartupOneTime_NotBackgroundPolling()
- {
- var startup = File.ReadAllText(FindFile(
- "server", "Plugins", "Admin.NET.Plugin.AiDOP", "Startup.cs"));
- Assert.Contains("s7Mig.DryRunAsync().GetAwaiter().GetResult();", startup);
- Assert.Contains("s7Mig.MigrateAsync().GetAwaiter().GetResult();", startup);
- Assert.Contains("AdoS7FlowAuthorityMigrationLog", startup);
- // 迁移服务不得被注册成后台常驻
- Assert.DoesNotContain("AddHostedService<Admin.NET.Plugin.AiDOP.Service.S7.S7LegacyFlowAuthorityMigrationService>", startup);
- }
- // ───────────── §43-20 / §38 / §39 回归 ─────────────
- /// <summary>§43-20 / §39:S7 通用审批旁路守卫必须仍然生效。</summary>
- [Fact]
- public void GenericApprovalGuard_StillCoversS7()
- {
- var dict = Slice(FlowTaskService(), "DedicatedEntryOnlyBizTypes = new()", "};");
- Assert.Contains("[\"S7_FQC_INSPBILL\"]", dict);
- }
- /// <summary>§38:本批**不得**把 S7 加入 TenantStrictRoleBizTypes(Guard 属下一治理边界)。</summary>
- [Fact]
- public void StrictRoleGuard_StillNotOpenedForS7()
- {
- var set = Slice(Engine(), "TenantStrictRoleBizTypes = new(StringComparer.Ordinal)", "};");
- Assert.DoesNotContain("S7_FQC_INSPBILL", set);
- Assert.Contains("\"IPQC_INSPECTION\"", set);
- Assert.Contains("\"S6_PROCESS_INSPECTION\"", set);
- }
- /// <summary>§37:本批不得清理跨租户绑定。</summary>
- [Theory]
- [InlineData("SysUserRole")]
- [InlineData("SysRole")]
- public void Migration_NeverWritesRoleTables(string table)
- {
- var code = CsCode(Service());
- foreach (var write in new[] { $"Deleteable<{table}>", $"AsUpdateable<{table}>", $"AsInsertable<{table}>", $"Insertable(new {table}" })
- Assert.DoesNotContain(write, code);
- }
- // ───────────── helpers ─────────────
- private static Dictionary<string, string> FullMap() => new(StringComparer.Ordinal)
- {
- [LegacySupervisorId] = SupervisorCode,
- [LegacyQeId] = QeCode,
- };
- private static string Rewrite(string before) =>
- S7LegacyFlowAuthorityMigrationService.RewriteAuthority(before, FullMap())!.After;
- private static List<string> ChangedNodesOf(string before) =>
- S7LegacyFlowAuthorityMigrationService.RewriteAuthority(before, FullMap())!.ChangedNodeIds;
- private static string? ApproverIdsOf(string snapshot, string nodeId) =>
- NodeProp(snapshot, nodeId, "approverIds");
- private static string? ApproverTypeOf(string snapshot, string nodeId) =>
- NodeProp(snapshot, nodeId, "approverType");
- private static string? NodeProp(string snapshot, string nodeId, string prop) =>
- JsonDocument.Parse(snapshot).RootElement.GetProperty("nodes").EnumerateArray()
- .First(n => n.GetProperty("id").GetString() == nodeId)
- .GetProperty("properties").GetProperty(prop).GetString();
- /// <summary>去注释视图:注释里会引用被淘汰的写法来解释「为何不那么做」,裸文本断言会误判。</summary>
- private static string CsCode(string src)
- {
- var s = System.Text.RegularExpressions.Regex.Replace(src, @"/\*.*?\*/", "", System.Text.RegularExpressions.RegexOptions.Singleline);
- var kept = s.Split('\n').Where(l => !l.TrimStart().StartsWith("//", StringComparison.Ordinal)
- && !l.TrimStart().StartsWith("///", StringComparison.Ordinal));
- return string.Join("\n", kept);
- }
- private static int CountOf(string src, string needle)
- {
- int n = 0, i = 0;
- while ((i = src.IndexOf(needle, i, StringComparison.Ordinal)) >= 0) { n++; i += needle.Length; }
- return n;
- }
- private static string Slice(string src, string from, string to)
- {
- var a = src.IndexOf(from, StringComparison.Ordinal);
- Assert.True(a >= 0, $"未找到起点:{from}");
- var b = src.IndexOf(to, a + from.Length, StringComparison.Ordinal);
- return b > a ? src[a..b] : src[a..];
- }
- 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));
- }
- }
|