S8AlertRuleWriteRetiredTests.cs 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  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-STEP6D-CFG-ALERT-LEGACY-WRITE-GUARD-CERT-1:钉住 CFG_ALERT 的
  12. /// 「历史兼容只读 + 写入口退役」契约。
  13. ///
  14. /// <para><b>不触任何数据库。</b> 这不是靠 mock 硬凑出来的——写方法在**任何 DB 访问之前**
  15. /// 就抛出,所以根本不需要可用的仓储。用 <see cref="RuntimeHelpers.GetUninitializedObject"/>
  16. /// 绕开构造函数(其参数 <c>SqlSugarRepository&lt;T&gt;</c> 的无参构造会触发 Furion.App
  17. /// 静态初始化,在无宿主的 xunit 进程内必抛 TypeInitializationException)。
  18. /// 若将来有人把守卫挪到 DB 访问之后,本测试会因 NullReferenceException 而失败——
  19. /// 这正是我们想要的信号:**守卫必须前置**。</para>
  20. /// </summary>
  21. public class S8AlertRuleWriteRetiredTests
  22. {
  23. private static S8AlertRuleService Svc() =>
  24. (S8AlertRuleService)RuntimeHelpers.GetUninitializedObject(typeof(S8AlertRuleService));
  25. private static readonly S8TrustedScope Scope = new(838257186181189L, 838257186320453L);
  26. private static AdoS8ConfigAlertRulesController Controller() => new(Svc(), null!);
  27. private static void AssertGone(IActionResult result)
  28. {
  29. var objectResult = Assert.IsType<ObjectResult>(result);
  30. Assert.Equal(StatusCodes.Status410Gone, objectResult.StatusCode);
  31. var message = objectResult.Value!.GetType().GetProperty("message")!.GetValue(objectResult.Value) as string;
  32. Assert.Equal(S8WriteRetiredException.UserMessage, message);
  33. }
  34. [Fact]
  35. public void CreateAsync_IsRetired()
  36. {
  37. var ex = Assert.Throws<S8WriteRetiredException>(
  38. () => { _ = Svc().CreateAsync(new AdoS8AlertRule { RuleCode = "X", SceneCode = "S1" }, Scope); });
  39. Assert.Equal(S8WriteRetiredException.UserMessage, ex.Message);
  40. }
  41. [Fact]
  42. public void UpdateAsync_IsRetired()
  43. {
  44. Assert.Throws<S8WriteRetiredException>(
  45. () => { _ = Svc().UpdateAsync(1L, new AdoS8AlertRule { RuleCode = "X", SceneCode = "S1" }, Scope); });
  46. }
  47. [Fact]
  48. public void DeleteAsync_IsRetired()
  49. {
  50. Assert.Throws<S8WriteRetiredException>(() => { _ = Svc().DeleteAsync(1L, Scope); });
  51. }
  52. [Fact]
  53. public async Task Controller_Create_Returns410_WithoutScopeOrRepositoryAccess()
  54. {
  55. AssertGone(await Controller().CreateAsync(new AdoS8AlertRule { RuleCode = "X", SceneCode = "S1" }));
  56. }
  57. [Theory]
  58. [InlineData(1L)]
  59. [InlineData(999999999L)]
  60. public async Task Controller_UpdateAndDelete_Return410_RegardlessOfId(long id)
  61. {
  62. var controller = Controller();
  63. AssertGone(await controller.UpdateAsync(id, new AdoS8AlertRule { RuleCode = "X", SceneCode = "S1" }));
  64. AssertGone(await controller.DeleteAsync(id));
  65. }
  66. /// <summary>越权 / 不存在的 Id 同样返回「已退役」,不得因 Id 不同而走回 404 分支。</summary>
  67. [Theory]
  68. [InlineData(0L)]
  69. [InlineData(1L)] // Demo 租户实际存在的 G01_TEST_ALERT
  70. [InlineData(999999999L)] // 不存在
  71. public void Writes_AreRetired_RegardlessOfId(long id)
  72. {
  73. Assert.Throws<S8WriteRetiredException>(() => { _ = Svc().DeleteAsync(id, Scope); });
  74. Assert.Throws<S8WriteRetiredException>(
  75. () => { _ = Svc().UpdateAsync(id, new AdoS8AlertRule { RuleCode = "X", SceneCode = "S1" }, Scope); });
  76. }
  77. /// <summary>读路径必须保留:历史兼容数据仍要能查。</summary>
  78. [Fact]
  79. public void ReadPath_IsPreserved()
  80. {
  81. Assert.NotNull(typeof(S8AlertRuleService).GetMethod("ListAsync", new[] { typeof(long), typeof(long) }));
  82. }
  83. /// <summary>
  84. /// 继承关系是防御性设计:只捕获 S8BizException 的旧调用方会安全降级为 400,而不是 500。
  85. /// </summary>
  86. [Fact]
  87. public void RetiredException_DerivesFromBizException()
  88. {
  89. Assert.True(typeof(S8BizException).IsAssignableFrom(typeof(S8WriteRetiredException)));
  90. Assert.False(typeof(S8NotFoundException).IsAssignableFrom(typeof(S8WriteRetiredException)));
  91. }
  92. /// <summary>用户文案必须同时说明「只读」与「当前权威是监控规则」,且不含内部实现细节。</summary>
  93. [Fact]
  94. public void UserMessage_StatesReadOnlyAndWatchRuleAuthority()
  95. {
  96. var m = S8WriteRetiredException.UserMessage;
  97. Assert.Contains("只读", m);
  98. Assert.Contains("监控规则", m);
  99. foreach (var leak in new[] { "Exception", "SqlSugar", "ado_s8_", "Service", "null" })
  100. Assert.DoesNotContain(leak, m, StringComparison.OrdinalIgnoreCase);
  101. }
  102. }