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; /// /// S8-LEGACY-RUNTIME-RETIREMENT-1(TASK B):钉住 CFG_DATASRC 的 /// 「历史只读 + 写入口退役 + 不再发起外连」契约。 /// /// 产品裁决:S8 = 数据中台消费层。物理数据源(连接串 / 主机 / 凭据)归数据平台治理, /// 不由租户 S8 管理员维护。只隐藏 UI 不够 —— API 仍可直接调用, /// 而 ado_s8_data_source.endpoint 存的是含凭据的真实连接串。 /// /// 不触任何数据库。 这不是靠 mock 硬凑出来的——写方法在**任何 DB 访问之前** /// 就抛出,所以根本不需要可用的仓储。沿用 的 /// 范式绕开构造函数 /// (其参数 SqlSugarRepository<T> 的无参构造会触发 Furion.App 静态初始化, /// 在无宿主的 xunit 进程内必抛 TypeInitializationException)。 /// 若将来有人把守卫挪到 DB 访问之后,本测试会因 NullReferenceException 而失败—— /// 这正是我们想要的信号:**守卫必须前置**。 /// 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(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(() => { _ = Svc().CreateAsync(Body(), Scope); }); Assert.Equal(S8DataSourceService.RetiredMessage, ex.Message); } [Fact] public void UpdateAsync_IsRetired() { Assert.Throws(() => { _ = Svc().UpdateAsync(1L, Body(), Scope); }); } [Fact] public void DeleteAsync_IsRetired() { Assert.Throws(() => { _ = Svc().DeleteAsync(1L, Scope); }); } /// /// 连接测试同样退役 —— 它会用库中 endpoint **真实建立外部数据库连接**, /// 正是本批要终止的「S8 维护物理连接」能力,比单纯的行更新更需要关掉。 /// [Fact] public void TestAsync_IsRetired() { Assert.Throws(() => { _ = Svc().TestAsync(1L, Scope); }); } /// 越权 / 不存在的 Id 同样返回「已退役」,不得因 Id 不同而走回 404 分支。 [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(() => { _ = Svc().DeleteAsync(id, Scope); }); Assert.Throws(() => { _ = Svc().UpdateAsync(id, Body(), Scope); }); Assert.Throws(() => { _ = 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)); } // ============================================================ // 读路径与结构保留 // ============================================================ /// 读路径必须保留:存量 Legacy 规则仍绑着这些行,排障与历史核对需要能查。 [Fact] public void ReadPath_IsPreserved() { Assert.NotNull(typeof(S8DataSourceService).GetMethod("ListAsync", new[] { typeof(long), typeof(long) })); } /// /// 本批只做 Runtime Retirement,不做物理清理: /// 实体与 Legacy 取数链必须仍然存在,否则存量规则的历史轨迹会断。 /// [Fact] public void LegacyBackendTypes_AreRetained_NotDeleted() { Assert.NotNull(typeof(AdoS8DataSource)); Assert.NotNull(typeof(S8DataSourceService)); } /// /// 结构性保证:服务不再持有任何能发起外部连接的依赖。 /// 「约定不用」会随时间失效,「拿不到工厂」不会。 /// [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); } /// /// 文案必须说明「由数据平台治理」这一裁决,且不泄漏内部实现细节。 /// 尤其不能出现连接串片段 —— 该表 endpoint 列存的正是含凭据的真实连接串。 /// [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); } }