mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-24 16:13:54 +08:00
fix(manager-api): make JSON collections type-safe
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
package xiaozhi.common.redis;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||
|
||||
class RedisSerializationTest {
|
||||
private final RedisSerializer<Object> serializer = RedisSerializer.json();
|
||||
|
||||
@Test
|
||||
void serverConfigRoundTripRestoresStringKeyedMaps() {
|
||||
Map<String, Object> features = new HashMap<>();
|
||||
features.put("addressBook", Map.of("enabled", true));
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
config.put("features", features);
|
||||
|
||||
Object restored = roundTrip(config);
|
||||
Map<String, Object> restoredConfig = JsonUtils.toStringObjectMap(restored);
|
||||
Map<String, Object> restoredFeatures = JsonUtils.toStringObjectMap(restoredConfig.get("features"));
|
||||
Map<String, Object> addressBook = JsonUtils.toStringObjectMap(restoredFeatures.get("addressBook"));
|
||||
|
||||
assertEquals(true, addressBook.get("enabled"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dictionaryListRoundTripRestoresDtoElements() {
|
||||
SysDictDataItem item = new SysDictDataItem();
|
||||
item.setName("enabled");
|
||||
item.setKey("1");
|
||||
|
||||
Object restored = roundTrip(new ArrayList<>(List.of(item)));
|
||||
List<SysDictDataItem> restoredItems = JsonUtils.toList(restored, SysDictDataItem.class);
|
||||
|
||||
assertInstanceOf(SysDictDataItem.class, restoredItems.get(0));
|
||||
assertEquals("enabled", restoredItems.get(0).getName());
|
||||
assertEquals("1", restoredItems.get(0).getKey());
|
||||
}
|
||||
|
||||
private Object roundTrip(Object value) {
|
||||
byte[] bytes = serializer.serialize(value);
|
||||
assertNotNull(bytes);
|
||||
Object restored = serializer.deserialize(bytes);
|
||||
assertNotNull(restored);
|
||||
return restored;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class JsonUtilsTest {
|
||||
|
||||
@Test
|
||||
void parsesTypedMapsAndPreservesNestedCollectionShapes() {
|
||||
Map<String, Object> map = JsonUtils.parseMap(
|
||||
"{\"enabled\":true,\"nested\":{\"items\":[{\"name\":\"tool\"}]}}");
|
||||
Map<?, ?> nested = assertInstanceOf(Map.class, map.get("nested"));
|
||||
List<?> items = assertInstanceOf(List.class, nested.get("items"));
|
||||
Map<?, ?> item = assertInstanceOf(Map.class, items.get(0));
|
||||
|
||||
assertEquals(true, map.get("enabled"));
|
||||
assertEquals("tool", item.get("name"));
|
||||
|
||||
List<Map<String, Object>> maps = JsonUtils.parseMapList(
|
||||
"[{\"name\":\"first\",\"values\":[1,2]},{\"name\":\"second\"}]");
|
||||
assertEquals("first", maps.get(0).get("name"));
|
||||
assertEquals(List.of(1, 2), maps.get(0).get("values"));
|
||||
assertEquals("second", maps.get(1).get("name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkedConvertersAreShallowMutableCopies() {
|
||||
List<Object> nested = new ArrayList<>(List.of(Map.of("name", "tool")));
|
||||
Map<String, Object> source = new LinkedHashMap<>();
|
||||
source.put("nested", nested);
|
||||
|
||||
Map<String, Object> map = JsonUtils.toStringObjectMap(source);
|
||||
List<Map<String, Object>> maps = JsonUtils.toStringObjectMapList(List.of(source));
|
||||
List<String> strings = JsonUtils.toList(Arrays.asList("a", null), String.class);
|
||||
|
||||
assertSame(nested, map.get("nested"));
|
||||
assertSame(nested, maps.get(0).get("nested"));
|
||||
map.put("enabled", true);
|
||||
maps.add(Map.of("name", "second"));
|
||||
strings.add("b");
|
||||
assertEquals(true, map.get("enabled"));
|
||||
assertEquals(2, maps.size());
|
||||
assertEquals(Arrays.asList("a", null, "b"), strings);
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesNullInputs() {
|
||||
assertNull(JsonUtils.parseMap(""));
|
||||
assertNull(JsonUtils.parseMapList(null));
|
||||
assertNull(JsonUtils.toStringObjectMap(null));
|
||||
assertNull(JsonUtils.toStringObjectMapList(null));
|
||||
assertNull(JsonUtils.toList(null, String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnexpectedJsonShapesAndRuntimeTypes() {
|
||||
assertThrows(RuntimeException.class, () -> JsonUtils.parseMap("[]"));
|
||||
assertThrows(RuntimeException.class, () -> JsonUtils.parseMapList("{}"));
|
||||
assertThrows(ClassCastException.class, () -> JsonUtils.toStringObjectMap(List.of()));
|
||||
assertThrows(ClassCastException.class, () -> JsonUtils.toStringObjectMap(Map.of(1, "value")));
|
||||
assertThrows(ClassCastException.class, () -> JsonUtils.toStringObjectMapList(List.of("value")));
|
||||
assertThrows(ClassCastException.class, () -> JsonUtils.toStringObjectMapList(Map.of()));
|
||||
assertThrows(ClassCastException.class, () -> JsonUtils.toList(List.of(1), String.class));
|
||||
assertThrows(ClassCastException.class, () -> JsonUtils.toList(Map.of(), String.class));
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package xiaozhi.modules.config.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.modules.agent.dao.AgentVoicePrintDao;
|
||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
import xiaozhi.modules.agent.service.AgentMcpAccessPointService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.correctword.service.CorrectWordFileService;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.sys.dto.SysParamsDTO;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
import xiaozhi.modules.timbre.service.TimbreService;
|
||||
import xiaozhi.modules.voiceclone.service.VoiceCloneService;
|
||||
|
||||
class ConfigServiceImplTest {
|
||||
|
||||
@Test
|
||||
void cachedServerConfigIsCheckedAsAStringKeyedMapWithoutChangingNestedValues() {
|
||||
RedisUtils redisUtils = mock(RedisUtils.class);
|
||||
Map<String, Object> nested = new HashMap<>();
|
||||
nested.put("enabled", true);
|
||||
Map<String, Object> cached = new HashMap<>();
|
||||
cached.put("features", nested);
|
||||
when(redisUtils.get(RedisKeys.getServerConfigKey())).thenReturn(cached);
|
||||
|
||||
ConfigServiceImpl service = newService(mock(SysParamsService.class), redisUtils);
|
||||
|
||||
Map<String, Object> result = service.getConfig(true);
|
||||
|
||||
assertNotSame(cached, result);
|
||||
assertSame(nested, result.get("features"));
|
||||
assertEquals(true, ((Map<?, ?>) result.get("features")).get("enabled"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nestedSystemParametersStillShareAndPopulateTheSameConfigBranch() {
|
||||
SysParamsService sysParamsService = mock(SysParamsService.class);
|
||||
SysParamsDTO enabled = parameter("server.features.enabled", "true", "boolean");
|
||||
SysParamsDTO labels = parameter("server.features.labels", "first;second", "array");
|
||||
when(sysParamsService.list(anyMap())).thenReturn(List.of(enabled, labels));
|
||||
ConfigServiceImpl service = newService(sysParamsService, mock(RedisUtils.class));
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
|
||||
Object returned = ReflectionTestUtils.invokeMethod(service, "buildConfig", config);
|
||||
|
||||
assertSame(config, returned);
|
||||
Map<?, ?> server = assertInstanceOf(Map.class, config.get("server"));
|
||||
Map<?, ?> features = assertInstanceOf(Map.class, server.get("features"));
|
||||
assertEquals(true, features.get("enabled"));
|
||||
assertEquals(List.of("first", "second"), features.get("labels"));
|
||||
}
|
||||
|
||||
private static SysParamsDTO parameter(String code, String value, String type) {
|
||||
SysParamsDTO parameter = new SysParamsDTO();
|
||||
parameter.setParamCode(code);
|
||||
parameter.setParamValue(value);
|
||||
parameter.setValueType(type);
|
||||
return parameter;
|
||||
}
|
||||
|
||||
private static ConfigServiceImpl newService(SysParamsService sysParamsService, RedisUtils redisUtils) {
|
||||
return new ConfigServiceImpl(
|
||||
sysParamsService,
|
||||
mock(DeviceService.class),
|
||||
mock(ModelConfigService.class),
|
||||
mock(AgentService.class),
|
||||
mock(AgentTemplateService.class),
|
||||
redisUtils,
|
||||
mock(TimbreService.class),
|
||||
mock(AgentPluginMappingService.class),
|
||||
mock(AgentMcpAccessPointService.class),
|
||||
mock(AgentContextProviderService.class),
|
||||
mock(VoiceCloneService.class),
|
||||
mock(AgentVoicePrintDao.class),
|
||||
mock(CorrectWordFileService.class));
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package xiaozhi.modules.sys.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||
|
||||
class SysDictDataServiceImplTest {
|
||||
|
||||
@Test
|
||||
void cachedDictionaryItemsAreCheckedAndReturnedAsDtos() {
|
||||
RedisUtils redisUtils = mock(RedisUtils.class);
|
||||
SysDictDataItem item = new SysDictDataItem();
|
||||
item.setName("enabled");
|
||||
item.setKey("1");
|
||||
List<SysDictDataItem> cached = new ArrayList<>(List.of(item));
|
||||
when(redisUtils.get(RedisKeys.getDictDataByTypeKey("status"))).thenReturn(cached);
|
||||
SysDictDataServiceImpl service = new SysDictDataServiceImpl(mock(SysUserDao.class), redisUtils);
|
||||
|
||||
List<SysDictDataItem> result = service.getDictDataByType("status");
|
||||
|
||||
assertNotSame(cached, result);
|
||||
assertEquals(List.of(item), result);
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package xiaozhi.modules.sys.service.impl;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.sys.dao.SysParamsDao;
|
||||
import xiaozhi.modules.sys.redis.SysParamsRedis;
|
||||
|
||||
class SysParamsServiceImplTest {
|
||||
|
||||
@Test
|
||||
void disablingAddressBookStillDeletesItsSystemPlugin() {
|
||||
SysParamsRedis sysParamsRedis = mock(SysParamsRedis.class);
|
||||
AgentPluginMappingService pluginMappingService = mock(AgentPluginMappingService.class);
|
||||
SysParamsDao sysParamsDao = mock(SysParamsDao.class);
|
||||
SysParamsServiceImpl service = new SysParamsServiceImpl(sysParamsRedis, pluginMappingService);
|
||||
ReflectionTestUtils.setField(service, "baseDao", sysParamsDao);
|
||||
|
||||
String currentConfig = "{\"features\":{\"addressBook\":{\"enabled\":true}}}";
|
||||
String newConfig = "{\"features\":{\"addressBook\":{\"enabled\":false}}}";
|
||||
when(sysParamsDao.getValueByCode(Constant.SYSTEM_WEB_MENU)).thenReturn(currentConfig);
|
||||
when(sysParamsDao.updateValueByCode(Constant.SYSTEM_WEB_MENU, newConfig)).thenReturn(1);
|
||||
|
||||
service.updateSystemWebMenu(newConfig);
|
||||
|
||||
verify(pluginMappingService).deleteByPluginId("SYSTEM_PLUGIN_CALL_DEVICE");
|
||||
verify(sysParamsDao).updateValueByCode(Constant.SYSTEM_WEB_MENU, newConfig);
|
||||
verify(sysParamsRedis).set(Constant.SYSTEM_WEB_MENU, newConfig);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user