|
|
@@ -39,4 +39,69 @@ public class EmployeePositionRulesTests
|
|
|
{
|
|
|
Assert.Equal(expected, EmployeePositionRules.Classify(raw));
|
|
|
}
|
|
|
+
|
|
|
+ [Theory]
|
|
|
+ [InlineData("WAREHOUSE", "WAREHOUSE")]
|
|
|
+ [InlineData(" PLANNER ", "PLANNER")]
|
|
|
+ // MySQL 默认排序规则不区分大小写,小写值不挡住就会绕过校验混进库里,故统一归一化为大写标准值。
|
|
|
+ [InlineData("warehouse", "WAREHOUSE")]
|
|
|
+ [InlineData("Other", "OTHER")]
|
|
|
+ [InlineData("WAREHOUSE1", null)]
|
|
|
+ [InlineData("WAREHOUS", null)]
|
|
|
+ [InlineData("QUALITY", null)]
|
|
|
+ [InlineData("", null)]
|
|
|
+ [InlineData(null, null)]
|
|
|
+ public void Normalize_OnlyAcceptsAllowedCodes(string? input, string? expected)
|
|
|
+ {
|
|
|
+ Assert.Equal(expected, EmployeePositionRules.Normalize(input));
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>岗位码白名单只有这五个:Classify 的全部返回值 + 人工可选值,与 KPI 的等值过滤口径对齐。</summary>
|
|
|
+ [Fact]
|
|
|
+ public void AllowedCodes_AreExactlyTheFiveNeutralCodes()
|
|
|
+ {
|
|
|
+ Assert.Equal(
|
|
|
+ new[] { "WAREHOUSE", "PRODUCTION", "PLANNER", "OTHER", "UNKNOWN" },
|
|
|
+ EmployeePositionRules.AllowedCodes);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>人工保存必须经过白名单,否则写错的岗位码会变成永远匹配不上 KPI 过滤的死值。</summary>
|
|
|
+ [Fact]
|
|
|
+ public void SavePositions_ValidatesAgainstWhitelist()
|
|
|
+ {
|
|
|
+ var service = File.ReadAllText(FindFile(
|
|
|
+ "server", "Plugins", "Admin.NET.Plugin.AiDOP", "DataPlatform", "MdpSourceMappingService.cs"));
|
|
|
+
|
|
|
+ Assert.Contains("EmployeePositionRules.Normalize(row.PositionCode)", service);
|
|
|
+ Assert.Contains("EmployeePositionRules.AllowedCodes", service);
|
|
|
+ // 旧写法直接把前端传入值落库,不得复活。
|
|
|
+ Assert.DoesNotContain("code = row.PositionCode.Trim()", service);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>前端下拉选项与后端白名单必须一致,否则页面能选出后端拒收的值。</summary>
|
|
|
+ [Fact]
|
|
|
+ public void FrontendPositionCodeOptions_MatchAllowedCodes()
|
|
|
+ {
|
|
|
+ var vue = File.ReadAllText(FindFile(
|
|
|
+ "Web", "src", "views", "aidop", "data-platform", "sourceMappings.vue"));
|
|
|
+
|
|
|
+ var expected = "const positionCodes = ["
|
|
|
+ + string.Join(", ", EmployeePositionRules.AllowedCodes.Select(code => $"'{code}'"))
|
|
|
+ + "];";
|
|
|
+ Assert.Contains(expected, vue);
|
|
|
+ // 岗位码不得退回自由文本输入。
|
|
|
+ Assert.DoesNotContain("<el-input v-model=\"row.positionCode\" />", vue);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string FindFile(params string[] parts)
|
|
|
+ {
|
|
|
+ var dir = new DirectoryInfo(AppContext.BaseDirectory);
|
|
|
+ while (dir != null)
|
|
|
+ {
|
|
|
+ var candidate = Path.Combine(new[] { dir.FullName }.Concat(parts).ToArray());
|
|
|
+ if (File.Exists(candidate)) return candidate;
|
|
|
+ dir = dir.Parent;
|
|
|
+ }
|
|
|
+ throw new FileNotFoundException(string.Join('/', parts));
|
|
|
+ }
|
|
|
}
|