| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115 |
- using System.Text.RegularExpressions;
- using Admin.NET.Plugin.AiDOP.Dto.S0.Manufacturing;
- using Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing;
- using SqlSugar;
- using Xunit;
- namespace Admin.NET.Plugin.AiDOP.Tests.S0.Tenant;
- /// <summary>
- /// S0-WORKORD-CONTROL-TENANT-UX-FIX 防回归契约。
- ///
- /// 守住四条:
- /// 1. Upsert 按 Id 定位必须先 tenant scope,禁止裸 GetByIdAsync(P0 越租户更新漏洞);
- /// 2. Id 不属于当前租户时必须 NotFound,不得落到 Update 分支;
- /// 3. GET / Upsert 的 Domain 均为可选,页面加载不依赖用户手输工厂域;
- /// 4. 唯一索引必须以 TenantId 打头,否则 CodeFirst 重建后会退化为跨租户唯一。
- ///
- /// 与同目录其他契约测试一致:用源码静态扫描,不需要真实 DB / HTTP 上下文。
- /// 真实的双租户读写隔离由集成冒烟(两个租户 token 实调)覆盖。
- /// </summary>
- public class S0WorkOrdControlTenantTests
- {
- private static string ControllerSource()
- {
- var dir = new DirectoryInfo(AppContext.BaseDirectory);
- while (dir != null && !Directory.Exists(Path.Combine(dir.FullName, "Admin.NET.Plugin.AiDOP", "Controllers", "S0")))
- dir = dir.Parent;
- Assert.NotNull(dir);
- var path = Path.Combine(dir!.FullName, "Admin.NET.Plugin.AiDOP", "Controllers", "S0",
- "Manufacturing", "AdoS0WorkOrdControlController.cs");
- Assert.True(File.Exists(path), $"未找到 {path}");
- return File.ReadAllText(path);
- }
- /// <summary>去掉注释行,避免注释里的说明文字造成误判。</summary>
- private static string StripComments(string src)
- {
- var withoutBlock = Regex.Replace(src, @"/\*.*?\*/", string.Empty, RegexOptions.Singleline);
- var lines = withoutBlock.Split('\n').Where(l => !l.TrimStart().StartsWith("//", StringComparison.Ordinal));
- return string.Join('\n', lines);
- }
- /// <summary>P0:不得使用未加租户条件的按 Id 查询。</summary>
- [Fact]
- public void Upsert_MustNotUseUnscopedGetById()
- {
- var src = StripComments(ControllerSource());
- Assert.DoesNotContain("GetByIdAsync", src);
- Assert.DoesNotContain("GetSingleAsync", src);
- // 按 Id 定位必须建立在 ScopedTo(tenantId) 之上。
- Assert.Matches(new Regex(@"ScopedTo\(tenantId\)\s*\r?\n?\s*\.Where\(x => x\.Id == dto\.Id"), src);
- }
- /// <summary>P0:Id 命中失败必须 404,绝不继续走到 Update。</summary>
- [Fact]
- public void Upsert_MustReturnNotFoundWhenIdBelongsToAnotherTenant()
- {
- var src = StripComments(ControllerSource());
- Assert.Contains("return NotFound();", src);
- var scopedIdIdx = src.IndexOf("x.Id == dto.Id", StringComparison.Ordinal);
- var notFoundIdx = src.IndexOf("return NotFound();", StringComparison.Ordinal);
- Assert.True(scopedIdIdx >= 0 && notFoundIdx > scopedIdIdx,
- "按 Id 的租户内查询之后必须紧跟 NotFound 分支。");
- var updateIdx = src.IndexOf("AsUpdateable", StringComparison.Ordinal);
- Assert.True(updateIdx > notFoundIdx, "NotFound 必须早于任何 Update 调用。");
- }
- /// <summary>每个 action 仍必须显式解析租户。</summary>
- [Fact]
- public void AllActions_ResolveTenantFirst()
- {
- var src = StripComments(ControllerSource());
- var actions = Regex.Matches(src, @"public async Task<IActionResult> (\w+)\(").Count;
- var resolves = Regex.Matches(src, @"AdoS0TenantScope\.TryResolveRequired").Count;
- Assert.True(actions > 0);
- Assert.Equal(actions, resolves);
- }
- /// <summary>Domain 必须可选:GET 不传也能取到当前租户配置。</summary>
- [Fact]
- public void DomainMustBeOptional_OnBothDtos()
- {
- var get = typeof(AdoS0WorkOrdControlGetDto).GetProperty(nameof(AdoS0WorkOrdControlGetDto.Domain))!;
- var upsert = typeof(AdoS0WorkOrdControlUpsertDto).GetProperty(nameof(AdoS0WorkOrdControlUpsertDto.Domain))!;
- Assert.Equal(typeof(string), get.PropertyType);
- Assert.Equal(typeof(string), upsert.PropertyType);
- Assert.Empty(get.GetCustomAttributes(typeof(System.ComponentModel.DataAnnotations.RequiredAttribute), true));
- Assert.Empty(upsert.GetCustomAttributes(typeof(System.ComponentModel.DataAnnotations.RequiredAttribute), true));
- // 查询里 Domain 只能作为可选筛选(WhereIF),不得是硬条件。
- var src = StripComments(ControllerSource());
- Assert.Contains("WhereIF(!string.IsNullOrWhiteSpace(domain)", src);
- }
- /// <summary>唯一索引必须以 TenantId 打头,否则跨租户抢占同一 Domain。</summary>
- [Fact]
- public void UniqueIndexMustLeadWithTenantId()
- {
- var index = typeof(AdoS0WorkOrdControl)
- .GetCustomAttributes(typeof(SugarIndexAttribute), inherit: true)
- .OfType<SugarIndexAttribute>()
- .Single();
- Assert.Equal(nameof(AdoS0WorkOrdControl.TenantId), index.IndexFields.Keys.First());
- Assert.Contains(nameof(AdoS0WorkOrdControl.Domain), index.IndexFields.Keys);
- Assert.True(index.IsUnique);
- }
- }
|