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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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; } }