package tech.easyflow.manuagent.model; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.fasterxml.jackson.databind.ObjectMapper; import com.mybatisflex.core.query.QueryWrapper; import java.time.OffsetDateTime; import java.util.List; import java.util.UUID; import org.junit.jupiter.api.Test; import org.springframework.boot.ApplicationRunner; import org.mockito.ArgumentCaptor; import tech.easyflow.manuagent.auth.UserService; import tech.easyflow.manuagent.config.AppProperties; import tech.easyflow.manuagent.entity.ModelConfigEntity; import tech.easyflow.manuagent.mapper.AppUserMapper; import tech.easyflow.manuagent.mapper.ModelAssignmentMapper; import tech.easyflow.manuagent.mapper.ModelConfigMapper; import tech.easyflow.manuagent.mapper.AgentRunMapper; /** * 验证模型管理接口使用最小字段投影,不把加密 API Key 读入普通请求内存。 */ class ModelServiceQueryTest { /** * 模型必须完全由管理接口和数据库维护,服务启动不得再通过 Key 文件自动灌入默认模型。 */ @Test void shouldNotInitializeModelConfigurationFromApplicationFiles() { assertThat(ApplicationRunner.class.isAssignableFrom(ModelService.class)).isFalse(); } /** * 模型列表只需要页面展示字段,查询 SQL 不得包含密文和密钥版本列。 */ @Test void shouldExcludeEncryptedKeyFromModelListQuery() { ModelConfigMapper modelMapper = mock(ModelConfigMapper.class); when(modelMapper.selectListByQuery(any(QueryWrapper.class))) .thenReturn(List.of(modelViewEntity())); ModelService service = new ModelService( modelMapper, mock(ModelAssignmentMapper.class), mock(AgentRunMapper.class), mock(UserService.class), mock(KeyCipher.class), new ObjectMapper()); List models = service.list(); ArgumentCaptor queryCaptor = ArgumentCaptor.forClass(QueryWrapper.class); verify(modelMapper).selectListByQuery(queryCaptor.capture()); String sql = queryCaptor.getValue().toSQL().toLowerCase(java.util.Locale.ROOT); assertThat(models).hasSize(1); assertThat(sql) .contains("api_key_hint", "capabilities_json", "is_default") .doesNotContain("api_key_ciphertext", "key_version"); } /** * 构造包含全部展示字段的实体,避免测试依赖数据库或模型密钥解密逻辑。 * * @return 模拟数据库返回的安全投影实体 */ private ModelConfigEntity modelViewEntity() { ModelConfigEntity entity = new ModelConfigEntity(); entity.setId(UUID.randomUUID()); entity.setName("测试模型"); entity.setProvider("OPENAI_COMPATIBLE"); entity.setBaseUrl("https://example.test"); entity.setModelId("model"); entity.setApiKeyHint("••••1234"); entity.setConfigJson("{}"); entity.setCapabilitiesJson("{\"contextWindow\":8192}"); entity.setEnabled(true); entity.setDefaultModel(true); entity.setUpdatedAt(OffsetDateTime.now()); return entity; } }