| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- using System.Runtime.CompilerServices;
- using Admin.NET.Plugin.AiDOP.Controllers.S8;
- using Admin.NET.Plugin.AiDOP.Entity.S8;
- using Admin.NET.Plugin.AiDOP.Infrastructure;
- using Admin.NET.Plugin.AiDOP.Service.S8;
- using Microsoft.AspNetCore.Http;
- using Microsoft.AspNetCore.Mvc;
- using Xunit;
- namespace Admin.NET.Plugin.AiDOP.Tests.S8;
- /// <summary>
- /// S8-LEGACY-RUNTIME-RETIREMENT-1(TASK B):钉住 CFG_DATASRC 的
- /// 「历史只读 + 写入口退役 + 不再发起外连」契约。
- ///
- /// 产品裁决:S8 = 数据中台消费层。物理数据源(连接串 / 主机 / 凭据)归数据平台治理,
- /// 不由租户 S8 管理员维护。只隐藏 UI 不够 —— API 仍可直接调用,
- /// 而 <c>ado_s8_data_source.endpoint</c> 存的是含凭据的真实连接串。
- ///
- /// <para><b>不触任何数据库。</b> 这不是靠 mock 硬凑出来的——写方法在**任何 DB 访问之前**
- /// 就抛出,所以根本不需要可用的仓储。沿用 <see cref="S8AlertRuleWriteRetiredTests"/> 的
- /// <see cref="RuntimeHelpers.GetUninitializedObject"/> 范式绕开构造函数
- /// (其参数 <c>SqlSugarRepository<T></c> 的无参构造会触发 Furion.App 静态初始化,
- /// 在无宿主的 xunit 进程内必抛 TypeInitializationException)。
- /// 若将来有人把守卫挪到 DB 访问之后,本测试会因 NullReferenceException 而失败——
- /// 这正是我们想要的信号:**守卫必须前置**。</para>
- /// </summary>
- public class S8DataSourceWriteRetiredTests
- {
- private static S8DataSourceService Svc() =>
- (S8DataSourceService)RuntimeHelpers.GetUninitializedObject(typeof(S8DataSourceService));
- private static readonly S8TrustedScope Scope = new(838257186181189L, 838257186320453L);
- private static AdoS8ConfigDataSourcesController Controller() => new(Svc(), null!);
- private static AdoS8DataSource Body() => new() { DataSourceCode = "UT_DS", Type = "SQL" };
- private static void AssertGone(IActionResult result)
- {
- var objectResult = Assert.IsType<ObjectResult>(result);
- Assert.Equal(StatusCodes.Status410Gone, objectResult.StatusCode);
- var message = objectResult.Value!.GetType().GetProperty("message")!.GetValue(objectResult.Value) as string;
- Assert.Equal(S8DataSourceService.RetiredMessage, message);
- }
- // ============================================================
- // Service 层:四个写入口全部退役
- // ============================================================
- [Fact]
- public void CreateAsync_IsRetired()
- {
- var ex = Assert.Throws<S8WriteRetiredException>(() => { _ = Svc().CreateAsync(Body(), Scope); });
- Assert.Equal(S8DataSourceService.RetiredMessage, ex.Message);
- }
- [Fact]
- public void UpdateAsync_IsRetired()
- {
- Assert.Throws<S8WriteRetiredException>(() => { _ = Svc().UpdateAsync(1L, Body(), Scope); });
- }
- [Fact]
- public void DeleteAsync_IsRetired()
- {
- Assert.Throws<S8WriteRetiredException>(() => { _ = Svc().DeleteAsync(1L, Scope); });
- }
- /// <summary>
- /// 连接测试同样退役 —— 它会用库中 endpoint **真实建立外部数据库连接**,
- /// 正是本批要终止的「S8 维护物理连接」能力,比单纯的行更新更需要关掉。
- /// </summary>
- [Fact]
- public void TestAsync_IsRetired()
- {
- Assert.Throws<S8WriteRetiredException>(() => { _ = Svc().TestAsync(1L, Scope); });
- }
- /// <summary>越权 / 不存在的 Id 同样返回「已退役」,不得因 Id 不同而走回 404 分支。</summary>
- [Theory]
- [InlineData(0L)]
- [InlineData(1L)] // 797 租户实际存在的 G01_TEST_DS
- [InlineData(5L)] // UAT 租户实际存在的 UATA-A01-DS
- [InlineData(999999999L)] // 不存在
- public void Writes_AreRetired_RegardlessOfId(long id)
- {
- Assert.Throws<S8WriteRetiredException>(() => { _ = Svc().DeleteAsync(id, Scope); });
- Assert.Throws<S8WriteRetiredException>(() => { _ = Svc().UpdateAsync(id, Body(), Scope); });
- Assert.Throws<S8WriteRetiredException>(() => { _ = Svc().TestAsync(id, Scope); });
- }
- // ============================================================
- // Controller 层:410 Gone
- // ============================================================
- [Fact]
- public async Task Controller_Create_Returns410_WithoutScopeOrRepositoryAccess()
- {
- // 注意 controller 的 S8TrustedScopeResolver 传的是 null:
- // 退役入口不得再去解析租户上下文,否则这里会 NullReferenceException。
- AssertGone(await Controller().CreateAsync(Body()));
- }
- [Theory]
- [InlineData(1L)]
- [InlineData(999999999L)]
- public async Task Controller_MutatingEndpoints_Return410_RegardlessOfId(long id)
- {
- var controller = Controller();
- AssertGone(await controller.UpdateAsync(id, Body()));
- AssertGone(await controller.DeleteAsync(id));
- AssertGone(await controller.TestAsync(id));
- }
- // ============================================================
- // 读路径与结构保留
- // ============================================================
- /// <summary>读路径必须保留:存量 Legacy 规则仍绑着这些行,排障与历史核对需要能查。</summary>
- [Fact]
- public void ReadPath_IsPreserved()
- {
- Assert.NotNull(typeof(S8DataSourceService).GetMethod("ListAsync", new[] { typeof(long), typeof(long) }));
- }
- /// <summary>
- /// 本批只做 Runtime Retirement,不做物理清理:
- /// 实体与 Legacy 取数链必须仍然存在,否则存量规则的历史轨迹会断。
- /// </summary>
- [Fact]
- public void LegacyBackendTypes_AreRetained_NotDeleted()
- {
- Assert.NotNull(typeof(AdoS8DataSource));
- Assert.NotNull(typeof(S8DataSourceService));
- }
- /// <summary>
- /// 结构性保证:服务不再持有任何能发起外部连接的依赖。
- /// 「约定不用」会随时间失效,「拿不到工厂」不会。
- /// </summary>
- [Fact]
- public void Service_NoLongerHoldsConnectionFactory()
- {
- var fieldTypes = typeof(S8DataSourceService)
- .GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)
- .Select(f => f.FieldType.Name)
- .ToArray();
- Assert.DoesNotContain("S8SqlSugarScopeFactory", fieldTypes);
- }
- /// <summary>
- /// 文案必须说明「由数据平台治理」这一裁决,且不泄漏内部实现细节。
- /// 尤其不能出现连接串片段 —— 该表 endpoint 列存的正是含凭据的真实连接串。
- /// </summary>
- [Fact]
- public void RetiredMessage_StatesDataPlatformAuthority_AndLeaksNothing()
- {
- var m = S8DataSourceService.RetiredMessage;
- Assert.Contains("数据平台", m);
- Assert.Contains("Dataset", m);
- foreach (var leak in new[] { "Exception", "SqlSugar", "ado_s8_", "Server=", "Password", "Pwd", "null" })
- Assert.DoesNotContain(leak, m, StringComparison.OrdinalIgnoreCase);
- }
- }
|