FormRenderNormalizeTest.java
4.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package com.xly.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xly.agent.AgentIdentity;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* 值规范化的**所见即所写**性质(2026-07-29 复审 HIGH 修复的回归锁):
* FK 多条命中必须报错列候选(绝不静默绑最短)、唯一精确同名优先、shown=实际绑定的记录名;
* 幻影列(字段字典有、物理表没有)必须整体拒绝,不得落到 varchar 分支跳过类型校验。
*/
class FormRenderNormalizeTest {
private static final AgentIdentity WHO = new AgentIdentity("tok", "u1", "brand", "sub", null);
private FormRenderService svc(FormResolverService resolver) {
return new FormRenderService(null, resolver, null, new ObjectMapper());
}
private FormResolverService resolverWith(Map<String, String> types, List<Map<String, Object>> fkCands) {
FormResolverService r = mock(FormResolverService.class);
when(r.columnTypes(anyString())).thenReturn(types);
when(r.resolveFkCandidates(anyString(), anyString(), anyString(), anyInt())).thenReturn(fkCands);
return r;
}
@Test
void ambiguousFkNameIsRejectedWithCandidates() {
FormResolverService r = resolverWith(Map.of("sCustomerId", "varchar"), List.of(
Map.of("sId", "id1", "name", "常州印刷"),
Map.of("sId", "id2", "name", "金陵印刷"),
Map.of("sId", "id3", "name", "连云港印刷")));
FormRenderService.Normalized n = svc(r).normalize("t", "sCustomerId", "elecustomer", "客户名称", "印刷", WHO);
assertNotNull(n.error, "多条命中必须报错,绝不静默绑最短匹配");
assertTrue(n.error.contains("常州印刷") && n.error.contains("金陵印刷"), "报错列出候选供用户挑");
assertNull(n.stored);
}
@Test
void exactNameWinsOverShorterFuzzyMatches() {
FormResolverService r = resolverWith(Map.of("sCustomerId", "varchar"), List.of(
Map.of("sId", "idShort", "name", "印刷"),
Map.of("sId", "idExact", "name", "金陵印刷")));
FormRenderService.Normalized n = svc(r).normalize("t", "sCustomerId", "elecustomer", "客户名称", "金陵印刷", WHO);
assertNull(n.error);
assertEquals("idExact", n.stored, "唯一精确同名直接命中");
assertEquals("金陵印刷", n.shown);
}
@Test
void uniquePartialMatchShowsResolvedRealName() {
FormResolverService r = resolverWith(Map.of("sCustomerId", "varchar"), List.of(
Map.of("sId", "id9", "name", "姑苏印务")));
FormRenderService.Normalized n = svc(r).normalize("t", "sCustomerId", "elecustomer", "客户名称", "姑苏", WHO);
assertNull(n.error);
assertEquals("id9", n.stored);
assertEquals("姑苏印务", n.shown, "卡片显示真正要绑定的记录名,而非用户原话");
}
@Test
void phantomColumnIsRejectedNotCoercedAsVarchar() {
// 字段字典解析出的列不在物理表:不得落 varchar 分支静默接受「五千」这类值
FormResolverService r = resolverWith(Map.of("dQty", "decimal"), List.of());
FormRenderService.Normalized n = svc(r).normalize("t", "sGhostCol", null, "数量", "五千", WHO);
assertNotNull(n.error);
assertTrue(n.error.contains("不是") && n.error.contains("可写字段"));
}
@Test
void numericColumnStillCoerces() {
FormResolverService real = mock(FormResolverService.class);
when(real.columnTypes(anyString())).thenReturn(Map.of("dQty", "decimal"));
when(real.coerce(eq("decimal"), anyString())).thenCallRealMethod();
FormRenderService.Normalized ok = svc(real).normalize("t", "dQty", null, "数量", "5,000", WHO);
assertNull(ok.error);
assertEquals("5000", String.valueOf(ok.stored));
FormRenderService.Normalized bad = svc(real).normalize("t", "dQty", null, "数量", "五千", WHO);
assertNotNull(bad.error, "数值列拒绝非数字");
}
}