You've already forked FrameTour-BE
- 移除已弃用的 DeviceCountRangeConditionStrategy 策略注册 - 修改 PuzzleElementFillEngine 执行方法调用方式,使用 getDynamicData 获取动态数据 - 在 PuzzleGenerateServiceImplTest 中引入 FillResult 类型并更新 mock 返回值结构 - 统一调整所有相关测试断言逻辑以匹配新返回的数据格式
447 lines
18 KiB
Java
447 lines
18 KiB
Java
package com.ycwl.basic.puzzle.fill;
|
|
|
|
import com.ycwl.basic.mapper.SourceMapper;
|
|
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
|
|
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleEntity;
|
|
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleItemEntity;
|
|
import com.ycwl.basic.puzzle.fill.condition.ConditionEvaluator;
|
|
import com.ycwl.basic.puzzle.fill.datasource.DataSourceResolver;
|
|
import com.ycwl.basic.puzzle.mapper.PuzzleFillRuleItemMapper;
|
|
import com.ycwl.basic.puzzle.mapper.PuzzleFillRuleMapper;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.DisplayName;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.extension.ExtendWith;
|
|
import org.mockito.InjectMocks;
|
|
import org.mockito.Mock;
|
|
import org.mockito.junit.jupiter.MockitoExtension;
|
|
import org.mockito.junit.jupiter.MockitoSettings;
|
|
import org.mockito.quality.Strictness;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
import static org.mockito.ArgumentMatchers.*;
|
|
import static org.mockito.Mockito.*;
|
|
|
|
/**
|
|
* 拼图元素填充引擎测试
|
|
*/
|
|
@DisplayName("拼图元素填充引擎测试")
|
|
@ExtendWith(MockitoExtension.class)
|
|
@MockitoSettings(strictness = Strictness.LENIENT)
|
|
class PuzzleElementFillEngineTest {
|
|
|
|
@Mock
|
|
private PuzzleFillRuleMapper ruleMapper;
|
|
|
|
@Mock
|
|
private PuzzleFillRuleItemMapper itemMapper;
|
|
|
|
@Mock
|
|
private SourceMapper sourceMapper;
|
|
|
|
@Mock
|
|
private ConditionEvaluator conditionEvaluator;
|
|
|
|
@Mock
|
|
private DataSourceResolver dataSourceResolver;
|
|
|
|
@InjectMocks
|
|
private PuzzleElementFillEngine engine;
|
|
|
|
@BeforeEach
|
|
void setUp() {
|
|
// 默认设置
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("当没有配置规则时应该返回空Map")
|
|
void shouldReturnEmptyMapWhenNoRulesConfigured() {
|
|
// Given
|
|
Long templateId = 1L;
|
|
Long faceId = 123L;
|
|
Long scenicId = 1L;
|
|
|
|
when(ruleMapper.listByTemplateId(templateId))
|
|
.thenReturn(new ArrayList<>());
|
|
|
|
// When
|
|
Map<String, String> result = engine.execute(templateId, faceId, scenicId).getDynamicData();
|
|
|
|
// Then
|
|
assertTrue(result.isEmpty());
|
|
verify(ruleMapper, times(1)).listByTemplateId(templateId);
|
|
verify(sourceMapper, never()).countDistinctDevicesByFaceId(anyLong());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("应该成功执行匹配的规则并填充多个元素")
|
|
void shouldExecuteMatchedRuleAndFillMultipleElements() {
|
|
// Given
|
|
Long templateId = 1L;
|
|
Long faceId = 123L;
|
|
Long scenicId = 1L;
|
|
|
|
// 模拟规则
|
|
PuzzleFillRuleEntity rule = createRule(1L, "4机位规则", 100);
|
|
when(ruleMapper.listByTemplateId(templateId))
|
|
.thenReturn(Arrays.asList(rule));
|
|
|
|
// 模拟机位数量和机位列表
|
|
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
|
|
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
|
|
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
|
|
|
|
// 模拟条件匹配
|
|
when(conditionEvaluator.evaluate(eq(rule), any())).thenReturn(true);
|
|
|
|
// 模拟规则明细
|
|
List<PuzzleFillRuleItemEntity> items = Arrays.asList(
|
|
createItem(1L, 1L, "sfp_1", "DEVICE_IMAGE", "{\"deviceIndex\":0}", "LATEST"),
|
|
createItem(2L, 1L, "sfp_2", "DEVICE_IMAGE", "{\"deviceIndex\":1}", "LATEST"),
|
|
createItem(3L, 1L, "sfp_3", "DEVICE_IMAGE", "{\"deviceIndex\":2}", "LATEST"),
|
|
createItem(4L, 1L, "sfp_4", "DEVICE_IMAGE", "{\"deviceIndex\":3}", "LATEST")
|
|
);
|
|
when(itemMapper.listByRuleId(1L)).thenReturn(items);
|
|
|
|
// 模拟数据源解析
|
|
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), anyString(), any()))
|
|
.thenReturn("https://oss.example.com/img1.jpg")
|
|
.thenReturn("https://oss.example.com/img2.jpg")
|
|
.thenReturn("https://oss.example.com/img3.jpg")
|
|
.thenReturn("https://oss.example.com/img4.jpg");
|
|
|
|
// When
|
|
Map<String, String> result = engine.execute(templateId, faceId, scenicId).getDynamicData();
|
|
|
|
// Then
|
|
assertEquals(4, result.size());
|
|
assertEquals("https://oss.example.com/img1.jpg", result.get("sfp_1"));
|
|
assertEquals("https://oss.example.com/img2.jpg", result.get("sfp_2"));
|
|
assertEquals("https://oss.example.com/img3.jpg", result.get("sfp_3"));
|
|
assertEquals("https://oss.example.com/img4.jpg", result.get("sfp_4"));
|
|
|
|
verify(conditionEvaluator, times(1)).evaluate(eq(rule), any());
|
|
verify(itemMapper, times(1)).listByRuleId(1L);
|
|
verify(dataSourceResolver, times(4)).resolve(anyString(), anyString(), anyString(), anyString(), any());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("缺少faceId时直接返回空结果")
|
|
void shouldReturnEmptyWhenRequiredIdsMissing() {
|
|
Map<String, String> result = engine.execute(1L, null, 1L).getDynamicData();
|
|
assertTrue(result.isEmpty());
|
|
verifyNoInteractions(ruleMapper, itemMapper, sourceMapper, conditionEvaluator, dataSourceResolver);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("规则无明细时应继续尝试下一条规则")
|
|
void shouldContinueWhenRuleHasNoItems() {
|
|
Long templateId = 1L;
|
|
Long faceId = 123L;
|
|
Long scenicId = 1L;
|
|
|
|
PuzzleFillRuleEntity highPriorityRule = createRule(1L, "高优先级无明细", 200);
|
|
PuzzleFillRuleEntity lowPriorityRule = createRule(2L, "低优先级有效", 100);
|
|
|
|
when(ruleMapper.listByTemplateId(templateId))
|
|
.thenReturn(Arrays.asList(highPriorityRule, lowPriorityRule));
|
|
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(1);
|
|
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(List.of(99L));
|
|
|
|
when(conditionEvaluator.evaluate(eq(highPriorityRule), any())).thenReturn(true);
|
|
when(conditionEvaluator.evaluate(eq(lowPriorityRule), any())).thenReturn(true);
|
|
|
|
when(itemMapper.listByRuleId(highPriorityRule.getId())).thenReturn(new ArrayList<>());
|
|
PuzzleFillRuleItemEntity lowRuleItem = createItem(10L, lowPriorityRule.getId(), "avatar",
|
|
"DEVICE_IMAGE", "{\"deviceIndex\":0}", "LATEST");
|
|
when(itemMapper.listByRuleId(lowPriorityRule.getId())).thenReturn(List.of(lowRuleItem));
|
|
|
|
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), anyString(), any()))
|
|
.thenReturn("https://oss.example.com/valid.png");
|
|
|
|
// When
|
|
Map<String, String> result = engine.execute(templateId, faceId, scenicId).getDynamicData();
|
|
|
|
assertEquals(1, result.size());
|
|
assertEquals("https://oss.example.com/valid.png", result.get("avatar"));
|
|
verify(conditionEvaluator, times(2)).evaluate(any(), any());
|
|
verify(itemMapper, times(2)).listByRuleId(anyLong());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("应该按优先级顺序评估规则并在匹配第一条后停止")
|
|
void shouldEvaluateRulesByPriorityAndStopAfterFirstMatch() {
|
|
// Given
|
|
Long templateId = 1L;
|
|
Long faceId = 123L;
|
|
Long scenicId = 1L;
|
|
|
|
// 模拟3条规则(按priority DESC排序)
|
|
PuzzleFillRuleEntity rule1 = createRule(1L, "高优先级规则", 100);
|
|
PuzzleFillRuleEntity rule2 = createRule(2L, "中优先级规则", 50);
|
|
PuzzleFillRuleEntity rule3 = createRule(3L, "低优先级规则", 10);
|
|
|
|
when(ruleMapper.listByTemplateId(templateId))
|
|
.thenReturn(Arrays.asList(rule1, rule2, rule3));
|
|
|
|
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
|
|
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
|
|
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
|
|
|
|
// 模拟第一条规则不匹配,第二条匹配
|
|
when(conditionEvaluator.evaluate(eq(rule1), any())).thenReturn(false);
|
|
when(conditionEvaluator.evaluate(eq(rule2), any())).thenReturn(true);
|
|
|
|
// 模拟规则2的明细
|
|
List<PuzzleFillRuleItemEntity> items = Arrays.asList(
|
|
createItem(1L, 2L, "sfp_1", "DEVICE_IMAGE", "{}", "LATEST")
|
|
);
|
|
when(itemMapper.listByRuleId(2L)).thenReturn(items);
|
|
|
|
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), anyString(), any()))
|
|
.thenReturn("https://oss.example.com/img.jpg");
|
|
|
|
// When
|
|
Map<String, String> result = engine.execute(templateId, faceId, scenicId).getDynamicData();
|
|
|
|
// Then
|
|
assertEquals(1, result.size());
|
|
assertEquals("https://oss.example.com/img.jpg", result.get("sfp_1"));
|
|
|
|
// 应该评估了rule1和rule2,但没有评估rule3(因为rule2匹配后停止)
|
|
verify(conditionEvaluator, times(1)).evaluate(eq(rule1), any());
|
|
verify(conditionEvaluator, times(1)).evaluate(eq(rule2), any());
|
|
verify(conditionEvaluator, never()).evaluate(eq(rule3), any());
|
|
|
|
verify(itemMapper, times(1)).listByRuleId(2L);
|
|
verify(itemMapper, never()).listByRuleId(1L);
|
|
verify(itemMapper, never()).listByRuleId(3L);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("当所有规则都不匹配时应该返回空Map")
|
|
void shouldReturnEmptyMapWhenNoRuleMatches() {
|
|
// Given
|
|
Long templateId = 1L;
|
|
Long faceId = 123L;
|
|
Long scenicId = 1L;
|
|
|
|
PuzzleFillRuleEntity rule1 = createRule(1L, "规则1", 100);
|
|
PuzzleFillRuleEntity rule2 = createRule(2L, "规则2", 50);
|
|
|
|
when(ruleMapper.listByTemplateId(templateId))
|
|
.thenReturn(Arrays.asList(rule1, rule2));
|
|
|
|
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
|
|
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
|
|
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
|
|
|
|
// 所有规则都不匹配
|
|
when(conditionEvaluator.evaluate(any(), any())).thenReturn(false);
|
|
|
|
// When
|
|
Map<String, String> result = engine.execute(templateId, faceId, scenicId).getDynamicData();
|
|
|
|
// Then
|
|
assertTrue(result.isEmpty());
|
|
verify(conditionEvaluator, times(2)).evaluate(any(), any());
|
|
verify(itemMapper, never()).listByRuleId(anyLong());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("当数据源解析返回null时应该跳过该元素")
|
|
void shouldSkipElementWhenDataSourceReturnsNull() {
|
|
// Given
|
|
Long templateId = 1L;
|
|
Long faceId = 123L;
|
|
Long scenicId = 1L;
|
|
|
|
PuzzleFillRuleEntity rule = createRule(1L, "测试规则", 100);
|
|
when(ruleMapper.listByTemplateId(templateId))
|
|
.thenReturn(Arrays.asList(rule));
|
|
|
|
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
|
|
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
|
|
when(conditionEvaluator.evaluate(eq(rule), any())).thenReturn(true);
|
|
|
|
List<PuzzleFillRuleItemEntity> items = Arrays.asList(
|
|
createItem(1L, 1L, "sfp_1", "DEVICE_IMAGE", "{}", "LATEST"),
|
|
createItem(2L, 1L, "sfp_2", "DEVICE_IMAGE", "{}", "LATEST")
|
|
);
|
|
when(itemMapper.listByRuleId(1L)).thenReturn(items);
|
|
|
|
// 第一个返回值,第二个返回null
|
|
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), anyString(), any()))
|
|
.thenReturn("https://oss.example.com/img1.jpg")
|
|
.thenReturn(null);
|
|
|
|
// When
|
|
Map<String, String> result = engine.execute(templateId, faceId, scenicId).getDynamicData();
|
|
|
|
// Then
|
|
assertEquals(1, result.size());
|
|
assertEquals("https://oss.example.com/img1.jpg", result.get("sfp_1"));
|
|
assertFalse(result.containsKey("sfp_2"));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("当数据源解析失败时应该使用fallbackValue")
|
|
void shouldUseFallbackValueWhenDataSourceFails() {
|
|
// Given
|
|
Long templateId = 1L;
|
|
Long faceId = 123L;
|
|
Long scenicId = 1L;
|
|
|
|
PuzzleFillRuleEntity rule = createRule(1L, "测试规则", 100);
|
|
when(ruleMapper.listByTemplateId(templateId))
|
|
.thenReturn(Arrays.asList(rule));
|
|
|
|
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
|
|
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
|
|
when(conditionEvaluator.evaluate(eq(rule), any())).thenReturn(true);
|
|
|
|
// 明细包含fallbackValue
|
|
PuzzleFillRuleItemEntity item = createItem(1L, 1L, "sfp_1", "DEVICE_IMAGE", "{}", "LATEST");
|
|
item.setFallbackValue("https://oss.example.com/default.jpg");
|
|
|
|
when(itemMapper.listByRuleId(1L)).thenReturn(Arrays.asList(item));
|
|
|
|
// DataSourceResolver内部会处理fallback,这里模拟返回fallback值
|
|
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), eq("https://oss.example.com/default.jpg"), any()))
|
|
.thenReturn("https://oss.example.com/default.jpg");
|
|
|
|
// When
|
|
Map<String, String> result = engine.execute(templateId, faceId, scenicId).getDynamicData();
|
|
|
|
// Then
|
|
assertEquals(1, result.size());
|
|
assertEquals("https://oss.example.com/default.jpg", result.get("sfp_1"));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("当规则匹配但没有明细时应该返回空Map")
|
|
void shouldReturnEmptyMapWhenRuleMatchesButHasNoItems() {
|
|
// Given
|
|
Long templateId = 1L;
|
|
Long faceId = 123L;
|
|
Long scenicId = 1L;
|
|
|
|
PuzzleFillRuleEntity rule = createRule(1L, "空明细规则", 100);
|
|
when(ruleMapper.listByTemplateId(templateId))
|
|
.thenReturn(Arrays.asList(rule));
|
|
|
|
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
|
|
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
|
|
when(conditionEvaluator.evaluate(eq(rule), any())).thenReturn(true);
|
|
|
|
// 规则没有明细
|
|
when(itemMapper.listByRuleId(1L)).thenReturn(new ArrayList<>());
|
|
|
|
// When
|
|
Map<String, String> result = engine.execute(templateId, faceId, scenicId).getDynamicData();
|
|
|
|
// Then
|
|
assertTrue(result.isEmpty());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("当发生异常时应该返回空Map并记录日志")
|
|
void shouldReturnEmptyMapAndLogWhenExceptionOccurs() {
|
|
// Given
|
|
Long templateId = 1L;
|
|
Long faceId = 123L;
|
|
Long scenicId = 1L;
|
|
|
|
when(ruleMapper.listByTemplateId(templateId))
|
|
.thenThrow(new RuntimeException("Database error"));
|
|
|
|
// When
|
|
Map<String, String> result = engine.execute(templateId, faceId, scenicId).getDynamicData();
|
|
|
|
// Then
|
|
assertTrue(result.isEmpty());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("应该支持DEVICE_ID_MATCH条件类型并正确填充")
|
|
void shouldSupportDeviceIdMatchCondition() {
|
|
// Given
|
|
Long templateId = 1L;
|
|
Long faceId = 123L;
|
|
Long scenicId = 1L;
|
|
|
|
// 模拟规则 - 使用DEVICE_ID_MATCH条件
|
|
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
|
|
rule.setId(1L);
|
|
rule.setRuleName("指定机位规则");
|
|
rule.setPriority(100);
|
|
rule.setConditionType("DEVICE_ID_MATCH");
|
|
rule.setConditionValue("{\"deviceIds\": [200, 300], \"matchMode\": \"ALL\"}");
|
|
rule.setEnabled(1);
|
|
|
|
when(ruleMapper.listByTemplateId(templateId))
|
|
.thenReturn(Arrays.asList(rule));
|
|
|
|
// 模拟机位数量和机位列表
|
|
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
|
|
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
|
|
|
|
// 模拟条件匹配
|
|
when(conditionEvaluator.evaluate(eq(rule), any())).thenReturn(true);
|
|
|
|
// 模拟规则明细
|
|
List<PuzzleFillRuleItemEntity> items = Arrays.asList(
|
|
createItem(1L, 1L, "sfp_1", "DEVICE_IMAGE", "{\"deviceIndex\":1}", "LATEST"),
|
|
createItem(2L, 1L, "sfp_2", "DEVICE_IMAGE", "{\"deviceIndex\":2}", "LATEST")
|
|
);
|
|
when(itemMapper.listByRuleId(1L)).thenReturn(items);
|
|
|
|
// 模拟数据源解析
|
|
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), anyString(), any()))
|
|
.thenReturn("https://oss.example.com/device200.jpg")
|
|
.thenReturn("https://oss.example.com/device300.jpg");
|
|
|
|
// When
|
|
Map<String, String> result = engine.execute(templateId, faceId, scenicId).getDynamicData();
|
|
|
|
// Then
|
|
assertEquals(2, result.size());
|
|
assertEquals("https://oss.example.com/device200.jpg", result.get("sfp_1"));
|
|
assertEquals("https://oss.example.com/device300.jpg", result.get("sfp_2"));
|
|
|
|
// 验证deviceIds被传递到ConditionContext
|
|
verify(conditionEvaluator, times(1)).evaluate(eq(rule), any());
|
|
verify(sourceMapper, times(1)).getDeviceIdsByFaceId(faceId);
|
|
}
|
|
|
|
// 辅助方法
|
|
private PuzzleFillRuleEntity createRule(Long id, String name, Integer priority) {
|
|
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
|
|
rule.setId(id);
|
|
rule.setRuleName(name);
|
|
rule.setPriority(priority);
|
|
rule.setConditionType("DEVICE_COUNT");
|
|
rule.setConditionValue("{\"deviceCount\": 4}");
|
|
rule.setEnabled(1);
|
|
return rule;
|
|
}
|
|
|
|
private PuzzleFillRuleItemEntity createItem(Long id, Long ruleId, String elementKey,
|
|
String dataSource, String sourceFilter, String sortStrategy) {
|
|
PuzzleFillRuleItemEntity item = new PuzzleFillRuleItemEntity();
|
|
item.setId(id);
|
|
item.setRuleId(ruleId);
|
|
item.setElementKey(elementKey);
|
|
item.setDataSource(dataSource);
|
|
item.setSourceFilter(sourceFilter);
|
|
item.setSortStrategy(sortStrategy);
|
|
item.setItemOrder(id.intValue());
|
|
return item;
|
|
}
|
|
}
|