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-STEP6D-CFG-ALERT-LEGACY-WRITE-GUARD-CERT-1:钉住 CFG_ALERT 的
/// 「历史兼容只读 + 写入口退役」契约。
///
/// 不触任何数据库。 这不是靠 mock 硬凑出来的——写方法在**任何 DB 访问之前**
/// 就抛出,所以根本不需要可用的仓储。用
/// 绕开构造函数(其参数 SqlSugarRepository<T> 的无参构造会触发 Furion.App
/// 静态初始化,在无宿主的 xunit 进程内必抛 TypeInitializationException)。
/// 若将来有人把守卫挪到 DB 访问之后,本测试会因 NullReferenceException 而失败——
/// 这正是我们想要的信号:**守卫必须前置**。
///
public class S8AlertRuleWriteRetiredTests
{
private static S8AlertRuleService Svc() =>
(S8AlertRuleService)RuntimeHelpers.GetUninitializedObject(typeof(S8AlertRuleService));
private static readonly S8TrustedScope Scope = new(838257186181189L, 838257186320453L);
private static AdoS8ConfigAlertRulesController Controller() => new(Svc(), null!);
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(S8WriteRetiredException.UserMessage, message);
}
[Fact]
public void CreateAsync_IsRetired()
{
var ex = Assert.Throws(
() => { _ = Svc().CreateAsync(new AdoS8AlertRule { RuleCode = "X", SceneCode = "S1" }, Scope); });
Assert.Equal(S8WriteRetiredException.UserMessage, ex.Message);
}
[Fact]
public void UpdateAsync_IsRetired()
{
Assert.Throws(
() => { _ = Svc().UpdateAsync(1L, new AdoS8AlertRule { RuleCode = "X", SceneCode = "S1" }, Scope); });
}
[Fact]
public void DeleteAsync_IsRetired()
{
Assert.Throws(() => { _ = Svc().DeleteAsync(1L, Scope); });
}
[Fact]
public async Task Controller_Create_Returns410_WithoutScopeOrRepositoryAccess()
{
AssertGone(await Controller().CreateAsync(new AdoS8AlertRule { RuleCode = "X", SceneCode = "S1" }));
}
[Theory]
[InlineData(1L)]
[InlineData(999999999L)]
public async Task Controller_UpdateAndDelete_Return410_RegardlessOfId(long id)
{
var controller = Controller();
AssertGone(await controller.UpdateAsync(id, new AdoS8AlertRule { RuleCode = "X", SceneCode = "S1" }));
AssertGone(await controller.DeleteAsync(id));
}
/// 越权 / 不存在的 Id 同样返回「已退役」,不得因 Id 不同而走回 404 分支。
[Theory]
[InlineData(0L)]
[InlineData(1L)] // Demo 租户实际存在的 G01_TEST_ALERT
[InlineData(999999999L)] // 不存在
public void Writes_AreRetired_RegardlessOfId(long id)
{
Assert.Throws(() => { _ = Svc().DeleteAsync(id, Scope); });
Assert.Throws(
() => { _ = Svc().UpdateAsync(id, new AdoS8AlertRule { RuleCode = "X", SceneCode = "S1" }, Scope); });
}
/// 读路径必须保留:历史兼容数据仍要能查。
[Fact]
public void ReadPath_IsPreserved()
{
Assert.NotNull(typeof(S8AlertRuleService).GetMethod("ListAsync", new[] { typeof(long), typeof(long) }));
}
///
/// 继承关系是防御性设计:只捕获 S8BizException 的旧调用方会安全降级为 400,而不是 500。
///
[Fact]
public void RetiredException_DerivesFromBizException()
{
Assert.True(typeof(S8BizException).IsAssignableFrom(typeof(S8WriteRetiredException)));
Assert.False(typeof(S8NotFoundException).IsAssignableFrom(typeof(S8WriteRetiredException)));
}
/// 用户文案必须同时说明「只读」与「当前权威是监控规则」,且不含内部实现细节。
[Fact]
public void UserMessage_StatesReadOnlyAndWatchRuleAuthority()
{
var m = S8WriteRetiredException.UserMessage;
Assert.Contains("只读", m);
Assert.Contains("监控规则", m);
foreach (var leak in new[] { "Exception", "SqlSugar", "ado_s8_", "Service", "null" })
Assert.DoesNotContain(leak, m, StringComparison.OrdinalIgnoreCase);
}
}