C1第一刀:flw_def.form_uuid启用+发布期form-center schema快照嵌入formContract+表达式键查①红牌(audience按数组解析)
build / build-push (push) Failing after 2m6s
build / build-push (push) Failing after 2m6s
This commit is contained in:
+177
@@ -0,0 +1,177 @@
|
||||
package com.yunhai.flowcenter.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yunhai.flowcenter.engine.FlowEngine;
|
||||
import com.yunhai.flowcenter.entity.FlwDef;
|
||||
import com.yunhai.flowcenter.enums.FlwEnums;
|
||||
import com.yunhai.flowcenter.repository.FlwAppRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwCallbackRegRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwRouterRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
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.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* publish() 的 C1 契约测试(2026-10-20 第一刀:发布期快照 + 表达式键查①红牌)。<br>
|
||||
* 覆盖:① 关联表单(form_uuid 非空)发布成功=content_json 顶层嵌 formContract 且 status=1;
|
||||
* ② 表达式变量 ∉ 快照字段 ∪ 内置变量 → 红牌阻断(点名节点/出口/变量);③ form_uuid 为空(存量线)
|
||||
* 不拉快照、行为不变;④ 快照拉取失败=发布阻断(可重试语义)。
|
||||
*/
|
||||
class FlowDefAdminServicePublishContractTest {
|
||||
|
||||
private static final Long SCH = 27L;
|
||||
|
||||
/** 含 CONDITION(EXPRESSION) 节点的合法定义:days/grade 是表单字段、schId 是内置变量 */
|
||||
private static final String JSON = "{\"start\":\"s\",\"nodes\":["
|
||||
+ "{\"id\":\"s\",\"type\":\"START\"},"
|
||||
+ "{\"id\":\"c\",\"type\":\"CONDITION\",\"name\":\"判断\",\"judgeMode\":\"EXPRESSION\"},"
|
||||
+ "{\"id\":\"a\",\"type\":\"APPROVAL\",\"name\":\"审1\",\"assignee\":{\"type\":\"USERS\",\"userIds\":[1]}},"
|
||||
+ "{\"id\":\"e\",\"type\":\"END\"}],"
|
||||
+ "\"edges\":["
|
||||
+ "{\"from\":\"s\",\"to\":\"c\"},"
|
||||
+ "{\"from\":\"c\",\"to\":\"a\",\"name\":\"准\",\"condition\":\"days > 3 && schId > 0\"},"
|
||||
+ "{\"from\":\"c\",\"to\":\"e\",\"name\":\"否\",\"condition\":\"grade == 'g1'\"},"
|
||||
+ "{\"from\":\"a\",\"to\":\"e\"}]}";
|
||||
|
||||
private FlwDefRepository defRepository;
|
||||
private FormCenterSchemaClient schemaClient;
|
||||
private FlowDefAdminService service;
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
defRepository = mock(FlwDefRepository.class);
|
||||
schemaClient = mock(FormCenterSchemaClient.class);
|
||||
service = new FlowDefAdminService(defRepository,
|
||||
mock(FlwRouterRepository.class),
|
||||
mock(FlwCallbackRegRepository.class),
|
||||
mock(FlwAppRepository.class),
|
||||
mock(FlwInstanceRepository.class),
|
||||
mock(FlowDeptAdminService.class),
|
||||
mock(FlowEngine.class),
|
||||
mapper,
|
||||
schemaClient);
|
||||
when(defRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
}
|
||||
|
||||
private FlwDef draft(String formUuid) {
|
||||
FlwDef d = new FlwDef();
|
||||
d.setId(9L);
|
||||
d.setSchId(SCH);
|
||||
d.setFlowGuid("g1");
|
||||
d.setCode("work-leave");
|
||||
d.setVersion(1);
|
||||
d.setStatus(FlwEnums.DefStatus.DRAFT.getCode());
|
||||
d.setAppKey("notice");
|
||||
d.setContentJson(JSON);
|
||||
d.setFormUuid(formUuid);
|
||||
return d;
|
||||
}
|
||||
|
||||
/** 快照:days(number)/grade(select enum g1,g2)/reason(text) */
|
||||
private Map<String, Object> snapshot() {
|
||||
Map<String, Object> snap = new LinkedHashMap<>();
|
||||
snap.put("formUuid", "FU1");
|
||||
snap.put("formVersion", 3);
|
||||
snap.put("audience", Arrays.asList("STUDENT", "TEACHER"));
|
||||
snap.put("fields", Arrays.asList(
|
||||
field("days", "number", true, null),
|
||||
field("grade", "select", false, Arrays.asList("g1", "g2")),
|
||||
field("reason", "text", true, null)));
|
||||
return snap;
|
||||
}
|
||||
|
||||
private static Map<String, Object> field(String key, String type, boolean required, List<String> enums) {
|
||||
Map<String, Object> f = new LinkedHashMap<>();
|
||||
f.put("key", key);
|
||||
f.put("type", type);
|
||||
f.put("required", required);
|
||||
if (enums != null) {
|
||||
f.put("enum", enums);
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishEmbedsFormContractSnapshotIntoVersionRow() throws Exception {
|
||||
when(defRepository.findById(9L)).thenReturn(java.util.Optional.of(draft("FU1")));
|
||||
when(schemaClient.fetchSchema(SCH, "FU1")).thenReturn(snapshot());
|
||||
|
||||
FlwDef published = service.publish(SCH, 9L);
|
||||
|
||||
assertEquals(FlwEnums.DefStatus.PUBLISHED.getCode(), published.getStatus());
|
||||
JsonNode contract = mapper.readTree(published.getContentJson()).get("formContract");
|
||||
assertNotNull(contract, "content_json 顶层必须嵌 formContract");
|
||||
assertEquals("FU1", contract.get("formUuid").asText());
|
||||
assertEquals(3, contract.get("formVersion").asInt());
|
||||
assertEquals(3, contract.get("fields").size(), "fields 扁平化嵌入");
|
||||
assertTrue(contract.get("audience").isArray(), "audience 按数组解析入快照");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownExpressionVariableIsRedCard() {
|
||||
FlwDef d = draft("FU1");
|
||||
// 出口条件改成引用快照外变量
|
||||
d.setContentJson(JSON.replace("grade == 'g1'", "level == 'high'"));
|
||||
when(defRepository.findById(9L)).thenReturn(java.util.Optional.of(d));
|
||||
when(schemaClient.fetchSchema(SCH, "FU1")).thenReturn(snapshot());
|
||||
|
||||
BizException ex = assertThrows(BizException.class, () -> service.publish(SCH, 9L));
|
||||
assertTrue(ex.getMessage().contains("level"), "红牌要点名未知变量:" + ex.getMessage());
|
||||
assertTrue(ex.getMessage().contains("键查"), "错误文案给可定位的键查提示");
|
||||
verify(defRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void builtinVariablesPassKeyCheck() {
|
||||
// days 与 schId(内置)都合法 → 上面的 happy path 已覆盖;此处单独断言纯内置表达式
|
||||
FlwDef d = draft("FU1");
|
||||
d.setContentJson(JSON.replace("grade == 'g1'", "bizType == 'x'"));
|
||||
when(defRepository.findById(9L)).thenReturn(java.util.Optional.of(d));
|
||||
when(schemaClient.fetchSchema(SCH, "FU1")).thenReturn(snapshot());
|
||||
|
||||
assertEquals(FlwEnums.DefStatus.PUBLISHED.getCode(), service.publish(SCH, 9L).getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyDefWithoutFormUuidSkipsSnapshotEntirely() {
|
||||
when(defRepository.findById(9L)).thenReturn(java.util.Optional.of(draft(null)));
|
||||
|
||||
FlwDef published = service.publish(SCH, 9L);
|
||||
|
||||
assertEquals(FlwEnums.DefStatus.PUBLISHED.getCode(), published.getStatus());
|
||||
assertEquals(JSON, published.getContentJson(), "存量线 content_json 一字不动");
|
||||
verify(schemaClient, never()).fetchSchema(anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void schemaUnavailableBlocksPublishAsRetryableBizError() {
|
||||
when(defRepository.findById(9L)).thenReturn(java.util.Optional.of(draft("FU1")));
|
||||
when(schemaClient.fetchSchema(SCH, "FU1"))
|
||||
.thenThrow(new FormCenterSchemaClient.SchemaUnavailableException("timeout", null));
|
||||
|
||||
BizException ex = assertThrows(BizException.class, () -> service.publish(SCH, 9L));
|
||||
assertTrue(ex.getMessage().contains("快照拉取失败"), "发布阻断文案可辨认:" + ex.getMessage());
|
||||
verify(defRepository, never()).save(any());
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,8 @@ class FlowDefAdminServiceSaveTest {
|
||||
mock(com.yunhai.flowcenter.repository.FlwInstanceRepository.class),
|
||||
mock(com.yunhai.flowcenter.service.FlowDeptAdminService.class),
|
||||
mock(FlowEngine.class),
|
||||
new ObjectMapper());
|
||||
new ObjectMapper(),
|
||||
mock(com.yunhai.flowcenter.service.FormCenterSchemaClient.class));
|
||||
when(defRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
// 归属应用白名单:非空 appKey 一律放行(校验拦截语义由专门用例覆盖)
|
||||
when(appRepository.isEnabledApp(any(), any())).thenReturn(true);
|
||||
|
||||
@@ -43,7 +43,8 @@ class FlowDefAdminServiceUpdateMetaTest {
|
||||
mock(com.yunhai.flowcenter.repository.FlwInstanceRepository.class),
|
||||
mock(com.yunhai.flowcenter.service.FlowDeptAdminService.class),
|
||||
mock(FlowEngine.class),
|
||||
new ObjectMapper());
|
||||
new ObjectMapper(),
|
||||
mock(com.yunhai.flowcenter.service.FormCenterSchemaClient.class));
|
||||
when(defRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
when(appRepository.isEnabledApp(any(), any())).thenReturn(true);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.yunhai.flowcenter.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yunhai.platform.openapi.core.YschOpenCall;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link FormCenterSchemaClient} 单元测试(2026-10-20 C1 第一刀)。<br>
|
||||
* 覆盖:① SchemaView(data) → 快照节的 fields 扁平化(分组展开、enum 收 options 值域);
|
||||
* ② audience <b>数组解析</b>(契约 v2 C5/G3 breaking)+ 旧单值串容错;③ 失败重试 2 次后转
|
||||
* SchemaUnavailableException(发布阻断语义)。
|
||||
*/
|
||||
class FormCenterSchemaClientTest {
|
||||
|
||||
private YschOpenCall openCall;
|
||||
private FormCenterSchemaClient client;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
openCall = mock(YschOpenCall.class);
|
||||
client = new FormCenterSchemaClient(openCall, new ObjectMapper());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> schemaData(String uuid, int version, List<String> audience) {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("formUuid", uuid);
|
||||
data.put("version", version);
|
||||
data.put("code", "leave-form");
|
||||
data.put("appCode", "svc-hall");
|
||||
data.put("audience", audience);
|
||||
data.put("groups", Arrays.asList(
|
||||
map("key", "base", "label", "基本信息", "fields", Arrays.asList(
|
||||
map("key", "days", "label", "天数", "type", "number", "required", true),
|
||||
map("key", "grade", "label", "年级", "type", "select", "required", false,
|
||||
"options", Arrays.asList(map("label", "高一", "value", "g1"),
|
||||
map("label", "高二", "value", "g2"))))),
|
||||
map("key", "extra", "label", "补充", "fields", Arrays.asList(
|
||||
map("key", "reason", "label", "事由", "type", "text", "required", true)))));
|
||||
return data;
|
||||
}
|
||||
|
||||
private static Map<String, Object> map(Object... kv) {
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
for (int i = 0; i + 1 < kv.length; i += 2) {
|
||||
m.put((String) kv[i], kv[i + 1]);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void flattensGroupsIntoFieldsAndCollectsEnumFromOptions() {
|
||||
when(openCall.get(anyString(), anyString(), anyMap(), eq(Map.class)))
|
||||
.thenReturn(schemaData("U1", 3, Arrays.asList("STUDENT", "TEACHER")));
|
||||
|
||||
Map<String, Object> snap = client.fetchSchema(27L, "U1");
|
||||
|
||||
assertEquals("U1", snap.get("formUuid"));
|
||||
assertEquals(3, snap.get("formVersion"));
|
||||
assertEquals(Arrays.asList("STUDENT", "TEACHER"), snap.get("audience"));
|
||||
List<Map<String, Object>> fields = (List<Map<String, Object>>) snap.get("fields");
|
||||
assertEquals(3, fields.size(), "两个分组的字段要扁平化到一处");
|
||||
Map<String, Object> grade = fields.stream()
|
||||
.filter(f -> "grade".equals(f.get("key"))).findFirst().orElseThrow(AssertionError::new);
|
||||
assertEquals("select", grade.get("type"));
|
||||
assertEquals(Boolean.FALSE, grade.get("required"));
|
||||
assertEquals(Arrays.asList("g1", "g2"), grade.get("enum"), "options 值域收进 enum(域查③的输入)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacySingleStringAudienceIsWrappedAsArray() {
|
||||
Map<String, Object> data = schemaData("U1", 1, null);
|
||||
data.put("audience", "STUDENT");
|
||||
when(openCall.get(anyString(), anyString(), anyMap(), eq(Map.class))).thenReturn(data);
|
||||
|
||||
Map<String, Object> snap = client.fetchSchema(27L, "U1");
|
||||
|
||||
assertEquals(Arrays.asList("STUDENT"), snap.get("audience"),
|
||||
"C5 切换窗口的旧单值串按一元素数组容错");
|
||||
}
|
||||
|
||||
@Test
|
||||
void retriesTwiceThenThrowsUnavailable() {
|
||||
when(openCall.get(anyString(), anyString(), anyMap(), eq(Map.class)))
|
||||
.thenThrow(new RuntimeException("connect refused"));
|
||||
|
||||
assertThrows(FormCenterSchemaClient.SchemaUnavailableException.class,
|
||||
() -> client.fetchSchema(27L, "U1"));
|
||||
verify(openCall, times(FormCenterSchemaClient.MAX_ATTEMPTS))
|
||||
.get(anyString(), anyString(), anyMap(), eq(Map.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void nullDataTreatedAsFailure() {
|
||||
when(openCall.get(anyString(), anyString(), anyMap(), eq(Map.class))).thenReturn(null);
|
||||
|
||||
assertThrows(FormCenterSchemaClient.SchemaUnavailableException.class,
|
||||
() -> client.fetchSchema(27L, "U1"));
|
||||
assertTrue(true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user