S8DataSourceWriteRetiredTests.cs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. using System.Runtime.CompilerServices;
  2. using Admin.NET.Plugin.AiDOP.Controllers.S8;
  3. using Admin.NET.Plugin.AiDOP.Entity.S8;
  4. using Admin.NET.Plugin.AiDOP.Infrastructure;
  5. using Admin.NET.Plugin.AiDOP.Service.S8;
  6. using Microsoft.AspNetCore.Http;
  7. using Microsoft.AspNetCore.Mvc;
  8. using Xunit;
  9. namespace Admin.NET.Plugin.AiDOP.Tests.S8;
  10. /// <summary>
  11. /// S8-LEGACY-RUNTIME-RETIREMENT-1(TASK B):钉住 CFG_DATASRC 的
  12. /// 「历史只读 + 写入口退役 + 不再发起外连」契约。
  13. ///
  14. /// 产品裁决:S8 = 数据中台消费层。物理数据源(连接串 / 主机 / 凭据)归数据平台治理,
  15. /// 不由租户 S8 管理员维护。只隐藏 UI 不够 —— API 仍可直接调用,
  16. /// 而 <c>ado_s8_data_source.endpoint</c> 存的是含凭据的真实连接串。
  17. ///
  18. /// <para><b>不触任何数据库。</b> 这不是靠 mock 硬凑出来的——写方法在**任何 DB 访问之前**
  19. /// 就抛出,所以根本不需要可用的仓储。沿用 <see cref="S8AlertRuleWriteRetiredTests"/> 的
  20. /// <see cref="RuntimeHelpers.GetUninitializedObject"/> 范式绕开构造函数
  21. /// (其参数 <c>SqlSugarRepository&lt;T&gt;</c> 的无参构造会触发 Furion.App 静态初始化,
  22. /// 在无宿主的 xunit 进程内必抛 TypeInitializationException)。
  23. /// 若将来有人把守卫挪到 DB 访问之后,本测试会因 NullReferenceException 而失败——
  24. /// 这正是我们想要的信号:**守卫必须前置**。</para>
  25. /// </summary>
  26. public class S8DataSourceWriteRetiredTests
  27. {
  28. private static S8DataSourceService Svc() =>
  29. (S8DataSourceService)RuntimeHelpers.GetUninitializedObject(typeof(S8DataSourceService));
  30. private static readonly S8TrustedScope Scope = new(838257186181189L, 838257186320453L);
  31. private static AdoS8ConfigDataSourcesController Controller() => new(Svc(), null!);
  32. private static AdoS8DataSource Body() => new() { DataSourceCode = "UT_DS", Type = "SQL" };
  33. private static void AssertGone(IActionResult result)
  34. {
  35. var objectResult = Assert.IsType<ObjectResult>(result);
  36. Assert.Equal(StatusCodes.Status410Gone, objectResult.StatusCode);
  37. var message = objectResult.Value!.GetType().GetProperty("message")!.GetValue(objectResult.Value) as string;
  38. Assert.Equal(S8DataSourceService.RetiredMessage, message);
  39. }
  40. // ============================================================
  41. // Service 层:四个写入口全部退役
  42. // ============================================================
  43. [Fact]
  44. public void CreateAsync_IsRetired()
  45. {
  46. var ex = Assert.Throws<S8WriteRetiredException>(() => { _ = Svc().CreateAsync(Body(), Scope); });
  47. Assert.Equal(S8DataSourceService.RetiredMessage, ex.Message);
  48. }
  49. [Fact]
  50. public void UpdateAsync_IsRetired()
  51. {
  52. Assert.Throws<S8WriteRetiredException>(() => { _ = Svc().UpdateAsync(1L, Body(), Scope); });
  53. }
  54. [Fact]
  55. public void DeleteAsync_IsRetired()
  56. {
  57. Assert.Throws<S8WriteRetiredException>(() => { _ = Svc().DeleteAsync(1L, Scope); });
  58. }
  59. /// <summary>
  60. /// 连接测试同样退役 —— 它会用库中 endpoint **真实建立外部数据库连接**,
  61. /// 正是本批要终止的「S8 维护物理连接」能力,比单纯的行更新更需要关掉。
  62. /// </summary>
  63. [Fact]
  64. public void TestAsync_IsRetired()
  65. {
  66. Assert.Throws<S8WriteRetiredException>(() => { _ = Svc().TestAsync(1L, Scope); });
  67. }
  68. /// <summary>越权 / 不存在的 Id 同样返回「已退役」,不得因 Id 不同而走回 404 分支。</summary>
  69. [Theory]
  70. [InlineData(0L)]
  71. [InlineData(1L)] // 797 租户实际存在的 G01_TEST_DS
  72. [InlineData(5L)] // UAT 租户实际存在的 UATA-A01-DS
  73. [InlineData(999999999L)] // 不存在
  74. public void Writes_AreRetired_RegardlessOfId(long id)
  75. {
  76. Assert.Throws<S8WriteRetiredException>(() => { _ = Svc().DeleteAsync(id, Scope); });
  77. Assert.Throws<S8WriteRetiredException>(() => { _ = Svc().UpdateAsync(id, Body(), Scope); });
  78. Assert.Throws<S8WriteRetiredException>(() => { _ = Svc().TestAsync(id, Scope); });
  79. }
  80. // ============================================================
  81. // Controller 层:410 Gone
  82. // ============================================================
  83. [Fact]
  84. public async Task Controller_Create_Returns410_WithoutScopeOrRepositoryAccess()
  85. {
  86. // 注意 controller 的 S8TrustedScopeResolver 传的是 null:
  87. // 退役入口不得再去解析租户上下文,否则这里会 NullReferenceException。
  88. AssertGone(await Controller().CreateAsync(Body()));
  89. }
  90. [Theory]
  91. [InlineData(1L)]
  92. [InlineData(999999999L)]
  93. public async Task Controller_MutatingEndpoints_Return410_RegardlessOfId(long id)
  94. {
  95. var controller = Controller();
  96. AssertGone(await controller.UpdateAsync(id, Body()));
  97. AssertGone(await controller.DeleteAsync(id));
  98. AssertGone(await controller.TestAsync(id));
  99. }
  100. // ============================================================
  101. // 读路径与结构保留
  102. // ============================================================
  103. /// <summary>读路径必须保留:存量 Legacy 规则仍绑着这些行,排障与历史核对需要能查。</summary>
  104. [Fact]
  105. public void ReadPath_IsPreserved()
  106. {
  107. Assert.NotNull(typeof(S8DataSourceService).GetMethod("ListAsync", new[] { typeof(long), typeof(long) }));
  108. }
  109. /// <summary>
  110. /// 本批只做 Runtime Retirement,不做物理清理:
  111. /// 实体与 Legacy 取数链必须仍然存在,否则存量规则的历史轨迹会断。
  112. /// </summary>
  113. [Fact]
  114. public void LegacyBackendTypes_AreRetained_NotDeleted()
  115. {
  116. Assert.NotNull(typeof(AdoS8DataSource));
  117. Assert.NotNull(typeof(S8DataSourceService));
  118. }
  119. /// <summary>
  120. /// 结构性保证:服务不再持有任何能发起外部连接的依赖。
  121. /// 「约定不用」会随时间失效,「拿不到工厂」不会。
  122. /// </summary>
  123. [Fact]
  124. public void Service_NoLongerHoldsConnectionFactory()
  125. {
  126. var fieldTypes = typeof(S8DataSourceService)
  127. .GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)
  128. .Select(f => f.FieldType.Name)
  129. .ToArray();
  130. Assert.DoesNotContain("S8SqlSugarScopeFactory", fieldTypes);
  131. }
  132. /// <summary>
  133. /// 文案必须说明「由数据平台治理」这一裁决,且不泄漏内部实现细节。
  134. /// 尤其不能出现连接串片段 —— 该表 endpoint 列存的正是含凭据的真实连接串。
  135. /// </summary>
  136. [Fact]
  137. public void RetiredMessage_StatesDataPlatformAuthority_AndLeaksNothing()
  138. {
  139. var m = S8DataSourceService.RetiredMessage;
  140. Assert.Contains("数据平台", m);
  141. Assert.Contains("Dataset", m);
  142. foreach (var leak in new[] { "Exception", "SqlSugar", "ado_s8_", "Server=", "Password", "Pwd", "null" })
  143. Assert.DoesNotContain(leak, m, StringComparison.OrdinalIgnoreCase);
  144. }
  145. }