| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- using Admin.NET.Plugin.AiDOP.DataPlatform;
- using Admin.NET.Plugin.AiDOP.DataPlatform.Executors;
- using Admin.NET.Plugin.AiDOP.Entity.DataPlatform;
- using Xunit;
- namespace Admin.NET.Plugin.AiDOP.Tests.DataPlatform;
- public class MdpDbSyncConfigContractTests
- {
- [Fact]
- public void Mapping_KeepsRawFields_AndAppliesDirectConstDefault()
- {
- var row = new Dictionary<string, object?> { ["ItemCode"] = "A", ["Qty"] = null };
- var mapped = MdpDbSyncRules.Apply(row,
- [
- new MdpFieldMapping { SourceField = "ItemCode", TargetField = "item_code", FieldType = "DIRECT", SortOrder = 1 },
- new MdpFieldMapping { SourceField = "Qty", TargetField = "qty", FieldType = "DIRECT", DefaultValue = "0", SortOrder = 2 },
- new MdpFieldMapping { SourceField = "", TargetField = "origin", FieldType = "CONST", ConstValue = "ERP", SortOrder = 3 }
- ]);
- Assert.Equal("A", mapped.Values["ItemCode"]);
- Assert.Equal("A", mapped.Values["item_code"]);
- Assert.Equal("0", mapped.Values["qty"]);
- Assert.Equal("ERP", mapped.Values["origin"]);
- Assert.Empty(mapped.Errors);
- }
- [Fact]
- public void Mapping_ReadsSafeJsonPath_AndRejectsScript()
- {
- var row = new Dictionary<string, object?> { ["payload"] = """{"buyer":{"name":"张三"}}""" };
- var ok = MdpDbSyncRules.Apply(row,
- [
- new MdpFieldMapping { SourceField = "payload", TargetField = "buyer_name", FieldType = "JSONPATH", TransformScript = "buyer.name" }
- ]);
- Assert.Equal("张三", ok.Values["buyer_name"]);
- var blocked = MdpDbSyncRules.Apply(row,
- [
- new MdpFieldMapping { SourceField = "payload", TargetField = "x", FieldType = "SCRIPT", TransformScript = "return 1" }
- ]);
- Assert.Contains(blocked.Errors, x => x.Contains("SCRIPT"));
- Assert.False(blocked.Values.TryGetValue("x", out var scriptValue) && scriptValue?.ToString() == "return 1");
- }
- [Fact]
- public void Mapping_RequiredMiss_IsRowError()
- {
- var mapped = MdpDbSyncRules.Apply(new Dictionary<string, object?>(),
- [
- new MdpFieldMapping { SourceField = "BillNo", TargetField = "bill_no", FieldType = "DIRECT", IsRequired = 1 }
- ]);
- Assert.Contains(mapped.Errors, x => x.Contains("bill_no"));
- }
- [Fact]
- public void BizKey_UsesMappedField()
- {
- var mapped = MdpDbSyncRules.Apply(
- new Dictionary<string, object?> { ["BillNo"] = "PO1" },
- [new MdpFieldMapping { SourceField = "BillNo", TargetField = "order_no", FieldType = "DIRECT" }]);
- Assert.Equal("PO1", MdpStagingWriter.BuildBizKey("order_no", mapped.Values));
- }
- [Fact]
- public void PhysicalTable_AndTransformRoute_AreIndependent()
- {
- var entity = new MdpEntity
- {
- SourceTableName = "third_party_po",
- TransformSourceTable = "PurOrdMaster"
- };
- Assert.Equal("PurOrdMaster", MdpDbPullExecutor.ResolveTransformSourceTable(entity));
- entity.TransformSourceTable = null;
- Assert.Equal("third_party_po", MdpDbPullExecutor.ResolveTransformSourceTable(entity));
- }
- [Fact]
- public void RebuildModules_ParseOnlySupportedDistinctModules()
- {
- var modules = MdpDbSyncRebuildTrigger.ParseModules("S1,S3|s1;S8 S7");
- Assert.Equal(["S1", "S3", "S7"], modules);
- }
- [Fact]
- public void DbEntity_FullCanOmitIncr_IncrRequiresTie()
- {
- var full = new MdpEntity
- {
- SourceTableName = "po",
- TargetTableName = "mdp_stg_po",
- SyncMode = "FULL",
- BizKeyExpr = "id",
- BatchSize = 100,
- TieBreakerColumn = "id"
- };
- Assert.Empty(MdpDbSyncRules.ValidateDbEntity(full, "DB"));
- var incr = new MdpEntity
- {
- SourceTableName = "po",
- TargetTableName = "mdp_stg_po",
- SyncMode = "INCR",
- BizKeyExpr = "id",
- BatchSize = 100
- };
- var issues = MdpDbSyncRules.ValidateDbEntity(incr, "DB");
- Assert.Contains(issues, x => x.Code == "TIE_BREAKER_MISSING");
- Assert.Contains(issues, x => x.Code == "INCR_COLUMN_MISSING");
- }
- [Fact]
- public void JobId_RejectsOverlengthInsteadOfHashing()
- {
- Assert.Null(MdpDbSyncRules.BuildJobId(1, new string('A', 80)));
- var jobId = MdpDbSyncRules.BuildJobId(12, "SRC_CUSTOMER");
- Assert.NotNull(jobId);
- Assert.True(jobId!.Length <= 64);
- Assert.StartsWith("job_mdp_dbsync_12_", jobId);
- }
- [Fact]
- public void Keyset_LegacyCursorIsInclusive_AndFullOrdersByTie()
- {
- var legacy = new MdpPullContext
- {
- UseKeysetCursor = true,
- CursorColumn = "UpdateTime",
- TieBreakerColumn = "RecID",
- CursorValue = "2026-01-01 00:00:00",
- LegacyCursorInclusive = true
- };
- var predicates = MdpDbPullExecutor.BuildKeysetPredicates(false, legacy, out _);
- Assert.Contains(predicates, x => x.Contains(">="));
- var sql = MdpDbPullExecutor.BuildSelectSql(new MdpEntity
- {
- SourceTableName = "po",
- TieBreakerColumn = "RecID"
- }, false, 10, new MdpPullContext { FullRefresh = true });
- Assert.Contains("ORDER BY `RecID`", sql);
- Assert.DoesNotContain("ORDER BY 1", sql);
- }
- [Fact]
- public void Dispatcher_FillsKeysetFromEntity_AndRejectsConflict()
- {
- var entity = new MdpEntity
- {
- EntityCode = "PO",
- SyncMode = "INCR",
- IncrColumn = "UpdateTime",
- TieBreakerColumn = "RecID",
- LastCursor = "2026-01-01"
- };
- var ctx = new MdpPullContext { RequireConfiguredDbSync = true };
- MdpSourcePullDispatcher.ApplyDbSyncDefaults(entity, new MdpSource { SourceType = "DB" }, ctx);
- Assert.True(ctx.UseKeysetCursor);
- Assert.Equal("UpdateTime", ctx.CursorColumn);
- Assert.True(ctx.LegacyCursorInclusive);
- Assert.Equal("2026-01-01", ctx.CursorValue);
- var conflict = new MdpPullContext { UseKeysetCursor = true, CursorColumn = "CreateTime", TieBreakerColumn = "Id" };
- Assert.Throws<InvalidOperationException>(() =>
- MdpSourcePullDispatcher.ApplyDbSyncDefaults(entity, new MdpSource { SourceType = "DB" }, conflict));
- }
- [Fact]
- public void Redact_HidesPhoneAndEmail()
- {
- var text = MdpDbSyncRules.Redact("手机 13812345678 邮箱 a@b.com");
- Assert.DoesNotContain("13812345678", text);
- Assert.DoesNotContain("a@b.com", text);
- }
- [Fact]
- public void SourceSave_UsesTenantAndEncryptsPassword()
- {
- var source = File.ReadAllText(Find("MdpSourceConfigService.cs"));
- Assert.Contains("x.TenantId == tenantId && x.SourceCode == code", source);
- Assert.Contains("CryptogramUtil.Encrypt", source);
- Assert.Contains("系统种子来源只能由迁移脚本维护", source);
- Assert.DoesNotContain("message = ex.Message", source);
- }
- [Fact]
- public void EntityUpdate_PreservesOmittedJobAndCursor()
- {
- var source = File.ReadAllText(Find("MdpSyncEntityConfigService.cs"));
- Assert.DoesNotContain("e.status = 1", source);
- Assert.Contains("if (!string.IsNullOrWhiteSpace(input.JobId))", source);
- Assert.Contains("if (input.BatchSize is > 0 and <= 10000)", source);
- Assert.DoesNotContain("LastCursor =", source);
- Assert.Contains("transform_source_table AS TransformSourceTable", source);
- }
- [Fact]
- public void BuiltInTemplate_CopyPreservesTargetAndMappings_ButHonorsExplicitRouteAndSource()
- {
- var source = File.ReadAllText(Find("MdpSyncEntityConfigService.cs"));
- Assert.Contains("input.TemplateEntityId", source);
- Assert.Contains("template?.TargetTableName", source);
- Assert.Contains("!string.IsNullOrWhiteSpace(input.TransformSourceTable)", source);
- Assert.Contains("!string.IsNullOrWhiteSpace(input.BizKeyExpr)", source);
- Assert.True(
- source.IndexOf("!string.IsNullOrWhiteSpace(input.SourceCode)", StringComparison.Ordinal)
- < source.IndexOf("if (input.SourceId > 0)", StringComparison.Ordinal));
- Assert.Contains("EntityId = id", source);
- Assert.Contains("await _db.Insertable(copies)", source);
- var drawer = File.ReadAllText(FindWeb("src/views/aidop/data-platform/components/SyncTaskEntityDrawer.vue"));
- Assert.Contains("sourceId: undefined", drawer);
- }
- [Fact]
- public void TaskRun_OnlyRebuildsAfterAllEntitiesAndRejectsQueueFailures()
- {
- var taskSource = File.ReadAllText(Find("MdpSyncTaskConfigService.cs"));
- Assert.True(
- taskSource.IndexOf("_rebuildTrigger.EnqueueForTaskAsync", StringComparison.Ordinal)
- > taskSource.IndexOf("_dbOperations.RunEntityAsync", StringComparison.Ordinal));
- Assert.True(
- taskSource.IndexOf("_neutralProjection.ProjectAsync", StringComparison.Ordinal)
- is var projectionAt && projectionAt > taskSource.IndexOf("_dbOperations.RunEntityAsync", StringComparison.Ordinal)
- && projectionAt < taskSource.IndexOf("_rebuildTrigger.EnqueueForTaskAsync", StringComparison.Ordinal));
- var triggerSource = File.ReadAllText(Find("MdpDbSyncRebuildTrigger.cs"));
- Assert.Contains("throw new InvalidOperationException", triggerSource);
- Assert.Contains("ParseModules(task.ConsumerModules)", triggerSource);
- Assert.Contains("if (status == 202)", triggerSource);
- Assert.DoesNotContain("status is 202 or 409", triggerSource);
- Assert.Contains("x.JobId == task.TaskCode || x.JobId == jobCode", taskSource);
- Assert.Contains("x.JobId == task.TaskCode || x.JobId == jobCode", triggerSource);
- var dispatcherSource = File.ReadAllText(FindPlugin("DataPlatform/Executors/IMdpSourcePullExecutor.cs"));
- Assert.Contains("FindPagedEntityAsync", dispatcherSource);
- Assert.Contains(".OrderByDescending(x => x.TenantId)", dispatcherSource);
- }
- [Fact]
- public void S1ToS4_StandardGatesHonorTenantRegisteredSource()
- {
- foreach (var relative in new[]
- {
- "Order/S1MdpSyncTransformService.cs",
- "Production/S2MdpSyncTransformService.cs",
- "Supply/S3MdpSyncTransformService.cs",
- "ProcurementExecution/S4MdpSyncTransformService.cs"
- })
- {
- var source = File.ReadAllText(FindPlugin(relative));
- Assert.Contains("GetSourceAsync", source);
- Assert.Contains("registeredSource", source);
- }
- }
- [Fact]
- public void SourceIdentity_PrefersRowSourceOverGlobalLegacyRegistry()
- {
- var sql = MdpSourceIdentity.Resolve("s.source_table", "s.source_system");
- Assert.True(
- sql.IndexOf("NULLIF(s.source_system", StringComparison.Ordinal)
- < sql.IndexOf("mdp_source_table_registry", StringComparison.Ordinal));
- }
- [Fact]
- public void DbConnectionError_IsActionableWithoutLeakingDriverDetails()
- {
- var auth = MdpDbConnectionError.Describe(
- new InvalidOperationException("Access denied for user 'secret_user'@'10.0.0.1'"));
- Assert.Equal("AUTH_FAILED", auth.Code);
- Assert.DoesNotContain("secret_user", auth.Message);
- Assert.DoesNotContain("10.0.0.1", auth.Message);
- var network = MdpDbConnectionError.Describe(new TimeoutException("connect timed out"));
- Assert.Equal("CONNECTION_FAILED", network.Code);
- Assert.Contains("网络连接失败", network.Message);
- }
- [Fact]
- public void DbSourceScope_UsesValidatedSourceAndRefreshesAfterConfigChange()
- {
- var scopeSource = File.ReadAllText(Find("MdpSourceScopeFactory.cs"));
- Assert.Contains("GetScopeAsync(MdpSource source", scopeSource);
- Assert.Contains("source.TenantId", scopeSource);
- Assert.Contains("source.Id", scopeSource);
- Assert.Contains("source.UpdateTime.Ticks", scopeSource);
- var metadataSource = File.ReadAllText(Find("MdpDbSourceMetadataService.cs"));
- Assert.Contains("sources/{sourceCode}/db/tables", metadataSource);
- Assert.Contains("_scopes.GetScopeAsync(source)", metadataSource);
- Assert.Contains("SafeMetadataError", metadataSource);
- }
- [Fact]
- public void NeutralProjection_SupportsDbSyncAndAuthoritySourceSwitch()
- {
- var source = File.ReadAllText(Find("InboundNeutralProjectionService.cs"));
- var dbProjection = File.ReadAllText(Find("MdpDbSyncNeutralProjection.cs"));
- Assert.Contains("MdpSourceIdentity.DbSync", dbProjection);
- Assert.Contains("entity.TransformSourceTable", dbProjection);
- Assert.Contains("@writtenBy", source);
- Assert.Contains("written_by=VALUES(written_by)", source);
- Assert.DoesNotContain("IF(source_system=VALUES(source_system)", source);
- Assert.Contains("source_system=VALUES(source_system)", source);
- var operation = File.ReadAllText(Find("MdpDbEntityOperationService.cs"));
- Assert.Contains("result.RowsError > 0", operation);
- Assert.Contains("本轮不触发中立层投影和模块重算", operation);
- Assert.DoesNotContain("BeginCacheLock(LockKey(entity), 0,", operation);
- }
- private static string Find(string file)
- {
- var dir = new DirectoryInfo(AppContext.BaseDirectory);
- while (dir != null)
- {
- var candidate = Path.Combine(dir.FullName, "server", "Plugins", "Admin.NET.Plugin.AiDOP", "DataPlatform", file);
- if (File.Exists(candidate)) return candidate;
- dir = dir.Parent;
- }
- throw new FileNotFoundException(file);
- }
- private static string FindPlugin(string relative)
- {
- var dir = new DirectoryInfo(AppContext.BaseDirectory);
- while (dir != null)
- {
- var candidate = Path.Combine(
- dir.FullName,
- "server",
- "Plugins",
- "Admin.NET.Plugin.AiDOP",
- relative.Replace('/', Path.DirectorySeparatorChar));
- if (File.Exists(candidate)) return candidate;
- dir = dir.Parent;
- }
- throw new FileNotFoundException(relative);
- }
- private static string FindWeb(string relative)
- {
- var dir = new DirectoryInfo(AppContext.BaseDirectory);
- while (dir != null)
- {
- var candidate = Path.Combine(
- dir.FullName,
- "Web",
- relative.Replace('/', Path.DirectorySeparatorChar));
- if (File.Exists(candidate)) return candidate;
- dir = dir.Parent;
- }
- throw new FileNotFoundException(relative);
- }
- }
|