Local review candidate only. Explicitly selected inherited prerequisites and prior repair patches; unrelated WIP and independent MQ fixes are excluded. No push or deployment authorized.
198 lines
9.1 KiB
Java
198 lines
9.1 KiB
Java
package com.yunhai.flowcenter.service;
|
||
|
||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||
import com.yunhai.platform.openapi.core.YschOpenCall;
|
||
import com.yunhai.platform.openapi.core.OpenApiException;
|
||
import com.yunhai.platform.openapi.core.OpenApiErrorCode;
|
||
import com.yunhai.platform.utils.exception.DException;
|
||
import org.junit.jupiter.params.ParameterizedTest;
|
||
import org.junit.jupiter.params.provider.ValueSource;
|
||
import org.junit.jupiter.params.provider.NullSource;
|
||
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", 3);
|
||
|
||
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", 3, null);
|
||
data.put("audience", "STUDENT");
|
||
when(openCall.get(anyString(), anyString(), anyMap(), eq(Map.class))).thenReturn(data);
|
||
|
||
Map<String, Object> snap = client.fetchSchema(27L, "U1", 3);
|
||
|
||
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", 3));
|
||
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", 3));
|
||
verify(openCall, times(FormCenterSchemaClient.MAX_ATTEMPTS))
|
||
.get(anyString(), anyString(), anyMap(), eq(Map.class));
|
||
}
|
||
|
||
@ParameterizedTest
|
||
@NullSource
|
||
@ValueSource(strings = {"0", "-1", "4", "3.5", "wrong", "2147483648"})
|
||
void malformedOrWrongReturnedVersionIsContractFailureWithoutRetry(String returnedVersion) {
|
||
Map<String, Object> data = schemaData("U1", 3, Arrays.asList("STUDENT"));
|
||
data.put("version", returnedVersion);
|
||
when(openCall.get(anyString(), anyString(), anyMap(), eq(Map.class))).thenReturn(data);
|
||
assertThrows(FormCenterSchemaClient.SchemaContractViolationException.class,
|
||
() -> client.fetchSchema(27L, "U1", 3));
|
||
verify(openCall, times(1)).get(anyString(), anyString(), anyMap(), eq(Map.class));
|
||
}
|
||
|
||
@ParameterizedTest
|
||
@NullSource
|
||
@ValueSource(strings = {"", "U2"})
|
||
void missingOrWrongReturnedUuidIsContractFailureWithoutRetry(String returnedUuid) {
|
||
when(openCall.get(anyString(), anyString(), anyMap(), eq(Map.class)))
|
||
.thenReturn(schemaData(returnedUuid, 3, Arrays.asList("STUDENT")));
|
||
assertThrows(FormCenterSchemaClient.SchemaContractViolationException.class,
|
||
() -> client.fetchSchema(27L, "U1", 3));
|
||
verify(openCall, times(1)).get(anyString(), anyString(), anyMap(), eq(Map.class));
|
||
}
|
||
|
||
@ParameterizedTest
|
||
@ValueSource(ints = {0, -1})
|
||
void illegalRequestedVersionMakesNoRemoteCall(int version) {
|
||
assertThrows(IllegalArgumentException.class, () -> client.fetchSchema(27L, "U1", version));
|
||
org.mockito.Mockito.verifyNoInteractions(openCall);
|
||
}
|
||
|
||
@Test
|
||
void resourceRejectionUsesEnvelopeCodeAndDoesNotRetry() {
|
||
// SDK 实际携带 OpenApiErrorCode,不会转换成本地 ErrorCode 枚举;文案刻意无关键词。
|
||
when(openCall.get(anyString(), anyString(), anyMap(), eq(Map.class)))
|
||
.thenThrow(new DException(new OpenApiErrorCode(2, "opaque")));
|
||
assertThrows(FormCenterSchemaClient.SchemaResourceUnavailableException.class,
|
||
() -> client.fetchSchema(27L, "U1", 3));
|
||
verify(openCall, times(1)).get(anyString(), anyString(), anyMap(), eq(Map.class));
|
||
}
|
||
|
||
@ParameterizedTest
|
||
@ValueSource(ints = {3, 4})
|
||
void authRejectionDoesNotBlindlyRetry(int code) {
|
||
when(openCall.get(anyString(), anyString(), anyMap(), eq(Map.class)))
|
||
.thenThrow(new DException(new OpenApiErrorCode(code, "opaque")));
|
||
assertThrows(FormCenterSchemaClient.SchemaAccessDeniedException.class,
|
||
() -> client.fetchSchema(27L, "U1", 3));
|
||
verify(openCall, times(1)).get(anyString(), anyString(), anyMap(), eq(Map.class));
|
||
}
|
||
|
||
@Test
|
||
void http404DoesNotBlindlyRetry() {
|
||
when(openCall.get(anyString(), anyString(), anyMap(), eq(Map.class)))
|
||
.thenThrow(new OpenApiException("opaque", 404, ""));
|
||
assertThrows(FormCenterSchemaClient.SchemaResourceUnavailableException.class,
|
||
() -> client.fetchSchema(27L, "U1", 3));
|
||
verify(openCall, times(1)).get(anyString(), anyString(), anyMap(), eq(Map.class));
|
||
}
|
||
|
||
@Test
|
||
void internalRemoteFailureRemainsRetryable() {
|
||
when(openCall.get(anyString(), anyString(), anyMap(), eq(Map.class)))
|
||
.thenThrow(new DException(new OpenApiErrorCode(6, "opaque")));
|
||
assertThrows(FormCenterSchemaClient.SchemaUnavailableException.class,
|
||
() -> client.fetchSchema(27L, "U1", 3));
|
||
verify(openCall, times(FormCenterSchemaClient.MAX_ATTEMPTS))
|
||
.get(anyString(), anyString(), anyMap(), eq(Map.class));
|
||
}
|
||
}
|