diff --git a/pom.xml b/pom.xml index 1249edaf5..3e5289c14 100644 --- a/pom.xml +++ b/pom.xml @@ -177,6 +177,12 @@ test + + com.h2database + h2 + test + + org.projectlombok diff --git a/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java b/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java index 4bf993014..3c88ce4b2 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java +++ b/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java @@ -27,7 +27,7 @@ public class RedisConfig { template.setConnectionFactory(factory); // 使用 String 序列化方式,序列化 KEY 。 template.setKeySerializer(RedisSerializer.string()); - // 使用 JSON 序列化方式(库是 FastJSON ),序列化 VALUE 。 + // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。 template.setValueSerializer(RedisSerializer.json()); return template; } diff --git a/src/main/java/cn/iocoder/dashboard/modules/infra/enums/InfErrorCodeConstants.java b/src/main/java/cn/iocoder/dashboard/modules/infra/enums/InfErrorCodeConstants.java index 3fc668e2c..53fc5f6a5 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/infra/enums/InfErrorCodeConstants.java +++ b/src/main/java/cn/iocoder/dashboard/modules/infra/enums/InfErrorCodeConstants.java @@ -11,7 +11,7 @@ public interface InfErrorCodeConstants { // ========== 参数配置 1001000000 ========== ErrorCode CONFIG_NOT_FOUND = new ErrorCode(1001000001, "参数配置不存在"); - ErrorCode CONFIG_NAME_DUPLICATE = new ErrorCode(1001000002, "参数配置 key 重复"); + ErrorCode CONFIG_KEY_DUPLICATE = new ErrorCode(1001000002, "参数配置 key 重复"); ErrorCode CONFIG_CAN_NOT_DELETE_SYSTEM_TYPE = new ErrorCode(1001000003, "不能删除类型为系统内置的参数配置"); ErrorCode CONFIG_GET_VALUE_ERROR_IF_SENSITIVE = new ErrorCode(1001000004, "不允许获取敏感配置到前端"); diff --git a/src/main/java/cn/iocoder/dashboard/modules/infra/service/config/impl/InfConfigServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/infra/service/config/impl/InfConfigServiceImpl.java index 719f2fd26..1f23e878a 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/infra/service/config/impl/InfConfigServiceImpl.java +++ b/src/main/java/cn/iocoder/dashboard/modules/infra/service/config/impl/InfConfigServiceImpl.java @@ -117,10 +117,10 @@ public class InfConfigServiceImpl implements InfConfigService { } // 如果 id 为空,说明不用比较是否为相同 id 的参数配置 if (id == null) { - throw ServiceExceptionUtil.exception(CONFIG_NAME_DUPLICATE); + throw ServiceExceptionUtil.exception(CONFIG_KEY_DUPLICATE); } if (!config.getId().equals(id)) { - throw ServiceExceptionUtil.exception(CONFIG_NAME_DUPLICATE); + throw ServiceExceptionUtil.exception(CONFIG_KEY_DUPLICATE); } } diff --git a/src/test/java/cn/iocoder/dashboard/modules/infra/service/config/InfConfigServiceImplTest.java b/src/test/java/cn/iocoder/dashboard/modules/infra/service/config/InfConfigServiceImplTest.java new file mode 100644 index 000000000..1302e4cfc --- /dev/null +++ b/src/test/java/cn/iocoder/dashboard/modules/infra/service/config/InfConfigServiceImplTest.java @@ -0,0 +1,81 @@ +package cn.iocoder.dashboard.modules.infra.service.config; + +import cn.iocoder.dashboard.common.exception.ServiceException; +import cn.iocoder.dashboard.modules.infra.controller.config.vo.InfConfigCreateReqVO; +import cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO; +import cn.iocoder.dashboard.modules.infra.dal.mysql.config.InfConfigMapper; +import cn.iocoder.dashboard.modules.infra.enums.config.InfConfigTypeEnum; +import cn.iocoder.dashboard.modules.infra.mq.producer.config.InfConfigProducer; +import cn.iocoder.dashboard.modules.infra.service.config.impl.InfConfigServiceImpl; +import cn.iocoder.dashboard.util.AssertUtils; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; + +import javax.annotation.Resource; + +import static cn.iocoder.dashboard.modules.infra.enums.InfErrorCodeConstants.CONFIG_KEY_DUPLICATE; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@SpringBootTest +@ActiveProfiles("unit-test") +@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +public class InfConfigServiceImplTest { + + @Resource + private InfConfigServiceImpl configService; + + @Resource + private InfConfigMapper configMapper; + + @MockBean + private InfConfigProducer configProducer; + + @Test + public void testCreateConfig_success() { + // 入参 + InfConfigCreateReqVO reqVO = new InfConfigCreateReqVO(); + reqVO.setGroup("test_group"); + reqVO.setName("test_name"); + reqVO.setValue("test_value"); + reqVO.setSensitive(true); + reqVO.setRemark("test_remark"); + reqVO.setKey("test_key"); + // mock + + // 调用 + Long configId = configService.createConfig(reqVO); + // 校验 + assertNotNull(configId); + // 校验记录的属性是否正确 + InfConfigDO config = configMapper.selectById(configId); + AssertUtils.assertEquals(reqVO, config); + assertEquals(InfConfigTypeEnum.CUSTOM.getType(), config.getType()); + // 校验调用 + verify(configProducer, times(1)).sendConfigRefreshMessage(); + } + + @Test + @Sql(statements = "INSERT INTO `inf_config`(`group`, `type`, `name`, `key`, `value`, `sensitive`) VALUES ('test_group', 1, 'test_name', 'test_key', 'test_value', 1);") + public void testCreateConfig_keyDuplicate() { + // 入参 + InfConfigCreateReqVO reqVO = new InfConfigCreateReqVO(); + reqVO.setGroup("test_group"); + reqVO.setName("test_name"); + reqVO.setValue("test_value"); + reqVO.setSensitive(true); + reqVO.setRemark("test_remark"); + reqVO.setKey("test_key"); + // mock + + // 调用 + ServiceException serviceException = assertThrows(ServiceException.class, () -> configService.createConfig(reqVO)); + // 断言 + AssertUtils.assertEquals(CONFIG_KEY_DUPLICATE, serviceException); + } + +} diff --git a/src/test/java/cn/iocoder/dashboard/modules/tool/dal/mysql/coegen/ToolInformationSchemaColumnMapperTest.java b/src/test/java/cn/iocoder/dashboard/modules/tool/dal/mysql/coegen/ToolInformationSchemaColumnMapperTest.java index 61832913d..8c52de093 100644 --- a/src/test/java/cn/iocoder/dashboard/modules/tool/dal/mysql/coegen/ToolInformationSchemaColumnMapperTest.java +++ b/src/test/java/cn/iocoder/dashboard/modules/tool/dal/mysql/coegen/ToolInformationSchemaColumnMapperTest.java @@ -3,19 +3,23 @@ package cn.iocoder.dashboard.modules.tool.dal.mysql.coegen; import cn.iocoder.dashboard.modules.tool.dal.dataobject.codegen.ToolSchemaColumnDO; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; import javax.annotation.Resource; import java.util.List; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertTrue; @SpringBootTest +@ActiveProfiles("unit-test") public class ToolInformationSchemaColumnMapperTest { @Resource private ToolSchemaColumnMapper toolInformationSchemaColumnMapper; @Test + @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) public void testSelectListByTableName() { List columns = toolInformationSchemaColumnMapper .selectListByTableName("inf_config"); diff --git a/src/test/java/cn/iocoder/dashboard/util/AssertUtils.java b/src/test/java/cn/iocoder/dashboard/util/AssertUtils.java new file mode 100644 index 000000000..ec4b4234c --- /dev/null +++ b/src/test/java/cn/iocoder/dashboard/util/AssertUtils.java @@ -0,0 +1,54 @@ +package cn.iocoder.dashboard.util; + +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.dashboard.common.exception.ErrorCode; +import cn.iocoder.dashboard.common.exception.ServiceException; +import org.junit.jupiter.api.Assertions; + +import java.lang.reflect.Field; +import java.util.Arrays; + +/** + * 单元测试,assert 断言工具类 + * + * @author 芋道源码 + */ +public class AssertUtils { + + /** + * 比对两个对象的属性是否一致 + * + * 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略 + * + * @param expected 期望对象 + * @param actual 实际对象 + */ + public static void assertEquals(Object expected, Object actual) { + Field[] expectedFields = ReflectUtil.getFields(expected.getClass()); + Arrays.stream(expectedFields).forEach(expectedField -> { + // 忽略不存在的属性 + Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName()); + if (actualField == null) { + return; + } + // 比对 + Assertions.assertEquals( + ReflectUtil.getFieldValue(expected, expectedField), + ReflectUtil.getFieldValue(actual, actualField), + String.format("Field(%s) 不匹配", expectedField.getName()) + ); + }); + } + + /** + * 比对抛出的 ServiceException 是否匹配 + * + * @param errorCode 错误码对象 + * @param serviceException 业务异常 + */ + public static void assertEquals(ErrorCode errorCode, ServiceException serviceException) { + Assertions.assertEquals(errorCode.getCode(), serviceException.getCode(), "错误码不匹配"); + Assertions.assertEquals(errorCode.getMessage(), serviceException.getMessage(), "错误提示不匹配"); + } + +} diff --git a/src/test/resources/application-unit-test.yaml b/src/test/resources/application-unit-test.yaml new file mode 100644 index 000000000..2990ef681 --- /dev/null +++ b/src/test/resources/application-unit-test.yaml @@ -0,0 +1,110 @@ +spring: + main: + lazy-initialization: true + + # 去除的自动配置项 + autoconfigure: + exclude: + - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration # 单元测试,禁用 SpringSecurity + - org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration # 单元测试,禁用 SpringSecurity + - org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration # 单元测试,禁用 Quartz + - com.baomidou.lock.spring.boot.autoconfigure.LockAutoConfiguration # 单元测试,禁用 Lock4j 分布式锁 + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + driver-class-name: org.h2.Driver + username: sa + password: + schema: classpath:sql/create_tables.sql # MySQL 转 H2 的语句,使用 https://www.jooq.org/translate/ 工具 + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 6379 # 端口 + database: 0 # 数据库索引 + +--- #################### 定时任务相关配置 #################### + +# Quartz 配置项,对应 QuartzProperties 配置类(单元测试,禁用 Quartz) + +--- #################### 配置中心相关配置 #################### + +# Apollo 配置中心 +apollo: + bootstrap: + enabled: false # 单元测试,禁用配置中心 + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +# Resilience4j 配置项 +resilience4j: + ratelimiter: + instances: + backendA: + limit-for-period: 1 # 每个周期内,允许的请求数。默认为 50 + limit-refresh-period: 60s # 每个周期的时长,单位:微秒。默认为 500 + timeout-duration: 1s # 被限流时,阻塞等待的时长,单位:微秒。默认为 5s + register-health-indicator: true # 是否注册到健康监测 + +--- #################### 监控相关配置 #################### + +# Actuator 监控端点的配置项 +management: + endpoints: + enabled-by-default: false + +# Spring Boot Admin 配置项 +spring: + boot: + admin: + # Spring Boot Admin Client 客户端的相关配置 + client: + enabled: false + # Spring Boot Admin Server 服务端的相关配置 + context-path: /admin # 配置 Spring + +# 日志文件配置 +logging: + file: + path: ${user.home}/logs/ # 日志文件的路径 + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yudao: + info: + version: 1.0.0 + base-package: cn.iocoder.dashboard + web: + api-prefix: /api + controller-package: ${yudao.info.base-package} + security: + token-header: Authorization + token-secret: abcdefghijklmnopqrstuvwxyz + token-timeout: 1d + session-timeout: 30m + mock-enable: true + mock-secret: test + swagger: + enable: false # 单元测试,禁用 Swagger + captcha: + timeout: 5m + width: 160 + height: 60 + file: + base-path: http://127.0.0.1:${server.port}/${yudao.web.api-prefix}/file/get/ + codegen: + base-package: ${yudao.info.base-package}.modules + db-schemas: ${spring.datasource.name} + xss: + enable: false + exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系 + - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 + - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 diff --git a/src/test/resources/application.yaml b/src/test/resources/application.yaml deleted file mode 100644 index 68d30a039..000000000 --- a/src/test/resources/application.yaml +++ /dev/null @@ -1,44 +0,0 @@ -spring: - application: - name: dashboard - - profiles: - active: local - - # Servlet 配置 - servlet: - # 文件上传相关配置项 - multipart: - max-file-size: 16MB # 单个文件大小 - max-request-size: 32MB # 设置总上传的文件大小 - - # Jackson 配置项 - jackson: - serialization: - write-dates-as-timestamps: true # 设置 Date 的格式,使用时间戳 - write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401 - write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳 - fail-on-empty-beans: false # 允许序列化无属性的 Bean - - main: - lazy-initialization: true - - # 去除的自动配置项 - autoconfigure: - exclude: - - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration - - org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration - - org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration - -# MyBatis Plus 的配置项 -mybatis-plus: - configuration: - map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。 -# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印日志 - global-config: - db-config: - id-type: AUTO # 自增 ID - logic-delete-value: 1 # 逻辑已删除值(默认为 1) - logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) - mapper-locations: classpath*:mapper/*.xml - type-aliases-package: ${yudao.info.base-package}.modules.*.dal.dataobject diff --git a/src/test/resources/sql/clean.sql b/src/test/resources/sql/clean.sql new file mode 100644 index 000000000..2887b4e9d --- /dev/null +++ b/src/test/resources/sql/clean.sql @@ -0,0 +1,9 @@ +-- inf 开头的 DB +DELETE FROM "inf_config"; + +-- sys 开头的 DB +DELETE FROM "sys_dept"; +DELETE FROM "sys_dict_data"; +DELETE FROM "sys_role"; +DELETE FROM "sys_role_menu"; +DELETE FROM "sys_menu"; diff --git a/src/test/resources/sql/create_tables.sql b/src/test/resources/sql/create_tables.sql new file mode 100644 index 000000000..7a24ed567 --- /dev/null +++ b/src/test/resources/sql/create_tables.sql @@ -0,0 +1,102 @@ +-- inf 开头的 DB + +CREATE TABLE "inf_config" ( + "id" int NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "group" varchar(50) NOT NULL, + "type" tinyint NOT NULL, + "name" varchar(100) NOT NULL DEFAULT '', + "key" varchar(100) NOT NULL DEFAULT '', + "value" varchar(500) NOT NULL DEFAULT '', + "sensitive" bit NOT NULL, + "remark" varchar(500) DEFAULT NULL, + "create_by" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "update_by" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '参数配置表'; + +-- sys 开头的 DB + +CREATE TABLE "sys_dept" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(30) NOT NULL DEFAULT '', + "parent_id" bigint NOT NULL DEFAULT '0', + "sort" int NOT NULL DEFAULT '0', + "leader" varchar(20) DEFAULT NULL, + "phone" varchar(11) DEFAULT NULL, + "email" varchar(50) DEFAULT NULL, + "status" tinyint NOT NULL, + "create_by" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "update_by" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '部门表'; + +CREATE TABLE "sys_dict_data" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "sort" int NOT NULL DEFAULT '0', + "label" varchar(100) NOT NULL DEFAULT '', + "value" varchar(100) NOT NULL DEFAULT '', + "dict_type" varchar(100) NOT NULL DEFAULT '', + "status" tinyint NOT NULL DEFAULT '0', + "remark" varchar(500) DEFAULT NULL, + "create_by" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "update_by" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '字典数据表'; + +CREATE TABLE "sys_role" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(30) NOT NULL, + "code" varchar(100) NOT NULL, + "sort" int NOT NULL, + "data_scope" tinyint NOT NULL DEFAULT '1', + "data_scope_dept_ids" varchar(500) NOT NULL DEFAULT '', + "status" tinyint NOT NULL, + "type" tinyint NOT NULL, + "remark" varchar(500) DEFAULT NULL, + "create_by" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "update_by" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '角色信息表'; + +CREATE TABLE "sys_role_menu" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "role_id" bigint NOT NULL, + "menu_id" bigint NOT NULL, + "create_by" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "update_by" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '角色和菜单关联表'; + +CREATE TABLE "sys_menu" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(50) NOT NULL, + "permission" varchar(100) NOT NULL DEFAULT '', + "menu_type" tinyint NOT NULL, + "sort" int NOT NULL DEFAULT '0', + "parent_id" bigint NOT NULL DEFAULT '0', + "path" varchar(200) DEFAULT '', + "icon" varchar(100) DEFAULT '#', + "component" varchar(255) DEFAULT NULL, + "status" tinyint NOT NULL DEFAULT '0', + "create_by" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "update_by" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '菜单权限表';