FormRenderNormalizeTest.java 4.52 KB
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, "数值列拒绝非数字");
    }
}