短信管理单元测试完善

This commit is contained in:
gaoqr
2025-10-14 09:03:58 +08:00
parent d8592dfce9
commit d6a60de583
42 changed files with 855 additions and 1480 deletions
@@ -14,6 +14,8 @@ import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockLoginUserSecurityContextFactory.class)
public @interface WithMockLoginUser {
long userId() default 0L;
String username() default "";
long deptId() default 0L;
@@ -22,6 +22,8 @@ public class WithMockLoginUserSecurityContextFactory implements WithSecurityCont
SecurityContext context = SecurityContextHolder.createEmptyContext();
LoginUser loginUser = new LoginUser();
long annotationUserId = annotation.userId();
loginUser.setId(annotationUserId == 0L ? RandomUtil.randomLong() : annotationUserId);
loginUser.setNickname(RandomUtil.randomString(10));
loginUser.setDeptId(RandomUtil.randomLong());
loginUser.setOrganId(RandomUtil.randomLong());
@@ -1,14 +1,12 @@
package com.cf.imes.module.system.api.sms;
import com.cf.imes.framework.common.pojo.CommonResult;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeValidateReqDTO;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import com.cf.imes.module.system.enums.ApiConstants;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -29,8 +27,4 @@ public interface SmsCodeApi {
@Operation(summary = "验证短信验证码,并进行使用")
CommonResult<Boolean> useSmsCode(@Valid @RequestBody SmsCodeUseReqDTO reqDTO);
@GetMapping(PREFIX + "/validate")
@Operation(summary = "检查验证码是否有效")
CommonResult<Boolean> validateSmsCode(@Valid @RequestBody SmsCodeValidateReqDTO reqDTO);
}
@@ -1,28 +0,0 @@
package com.cf.imes.module.system.api.sms;
import com.cf.imes.framework.common.pojo.CommonResult;
import com.cf.imes.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO;
import com.cf.imes.module.system.enums.ApiConstants;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import jakarta.validation.Valid;
@FeignClient(name = ApiConstants.NAME) // TODO 晨丰:fallbackFactory =
@Tag(name = "RPC 服务 - 短信发送")
public interface SmsSendApi {
String PREFIX = ApiConstants.PREFIX + "/sms/send";
@PostMapping(PREFIX + "/send-single-admin")
@Operation(summary = "发送单条短信给 Admin 用户", description = "在 mobile 为空时,使用 userId 加载对应 Admin 的手机号")
CommonResult<Long> sendSingleSmsToAdmin(@Valid @RequestBody SmsSendSingleToUserReqDTO reqDTO);
@PostMapping(PREFIX + "/send-single-member")
@Operation(summary = "发送单条短信给 Member 用户", description = "在 mobile 为空时,使用 userId 加载对应 Member 的手机号")
CommonResult<Long> sendSingleSmsToMember(@Valid @RequestBody SmsSendSingleToUserReqDTO reqDTO);
}
@@ -3,7 +3,6 @@ package com.cf.imes.module.system.api.sms;
import com.cf.imes.framework.common.pojo.CommonResult;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeValidateReqDTO;
import com.cf.imes.module.system.service.sms.SmsCodeService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
@@ -31,10 +30,4 @@ public class SmsCodeApiImpl implements SmsCodeApi {
return success(true);
}
@Override
public CommonResult<Boolean> validateSmsCode(SmsCodeValidateReqDTO reqDTO) {
smsCodeService.validateSmsCode(reqDTO);
return success(true);
}
}
@@ -1,32 +0,0 @@
package com.cf.imes.module.system.api.sms;
import com.cf.imes.framework.common.pojo.CommonResult;
import com.cf.imes.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO;
import com.cf.imes.module.system.service.sms.SmsSendService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;
import jakarta.annotation.Resource;
import static com.cf.imes.framework.common.pojo.CommonResult.success;
@RestController // 提供 RESTful API 接口,给 Feign 调用
@Validated
public class SmsSendApiImpl implements SmsSendApi {
@Resource
private SmsSendService smsSendService;
@Override
public CommonResult<Long> sendSingleSmsToAdmin(SmsSendSingleToUserReqDTO reqDTO) {
return success(smsSendService.sendSingleSmsToAdmin(reqDTO.getMobile(), reqDTO.getUserId(),
reqDTO.getTemplateCode(), reqDTO.getTemplateParams()));
}
@Override
public CommonResult<Long> sendSingleSmsToMember(SmsSendSingleToUserReqDTO reqDTO) {
return success(smsSendService.sendSingleSmsToMember(reqDTO.getMobile(), reqDTO.getUserId(),
reqDTO.getTemplateCode(), reqDTO.getTemplateParams()));
}
}
@@ -1,82 +0,0 @@
package com.cf.imes.module.system.controller.admin.sms;
import com.cf.imes.framework.common.pojo.CommonResult;
import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.framework.common.util.object.BeanUtils;
import com.cf.imes.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO;
import com.cf.imes.module.system.controller.admin.sms.vo.channel.SmsChannelRespVO;
import com.cf.imes.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO;
import com.cf.imes.module.system.controller.admin.sms.vo.channel.SmsChannelSimpleRespVO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsChannelDO;
import com.cf.imes.module.system.service.sms.SmsChannelService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import java.util.Comparator;
import java.util.List;
import static com.cf.imes.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 短信渠道")
@RestController
@RequestMapping("system/sms-channel")
public class SmsChannelController {
@Resource
private SmsChannelService smsChannelService;
@PostMapping("/create")
@Operation(summary = "创建短信渠道")
@PreAuthorize("@ss.hasPermission('system:sms-channel:create')")
public CommonResult<Long> createSmsChannel(@Valid @RequestBody SmsChannelSaveReqVO createReqVO) {
return success(smsChannelService.createSmsChannel(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新短信渠道")
@PreAuthorize("@ss.hasPermission('system:sms-channel:update')")
public CommonResult<Boolean> updateSmsChannel(@Valid @RequestBody SmsChannelSaveReqVO updateReqVO) {
smsChannelService.updateSmsChannel(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除短信渠道")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('system:sms-channel:delete')")
public CommonResult<Boolean> deleteSmsChannel(@RequestParam("id") Long id) {
smsChannelService.deleteSmsChannel(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得短信渠道")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('system:sms-channel:query')")
public CommonResult<SmsChannelRespVO> getSmsChannel(@RequestParam("id") Long id) {
SmsChannelDO channel = smsChannelService.getSmsChannel(id);
return success(BeanUtils.toBean(channel, SmsChannelRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得短信渠道分页")
@PreAuthorize("@ss.hasPermission('system:sms-channel:query')")
public CommonResult<PageResult<SmsChannelRespVO>> getSmsChannelPage(@Valid SmsChannelPageReqVO pageVO) {
PageResult<SmsChannelDO> pageResult = smsChannelService.getSmsChannelPage(pageVO);
return success(BeanUtils.toBean(pageResult, SmsChannelRespVO.class));
}
@GetMapping({"/list-all-simple", "/simple-list"})
@Operation(summary = "获得短信渠道精简列表", description = "包含被禁用的短信渠道")
public CommonResult<List<SmsChannelSimpleRespVO>> getSimpleSmsChannelList() {
List<SmsChannelDO> list = smsChannelService.getSmsChannelList();
list.sort(Comparator.comparing(SmsChannelDO::getId));
return success(BeanUtils.toBean(list, SmsChannelSimpleRespVO.class));
}
}
@@ -1,5 +1,6 @@
package com.cf.imes.module.system.controller.admin.sms;
import com.cf.imes.framework.common.enums.UserTypeEnum;
import com.cf.imes.framework.common.pojo.CommonResult;
import com.cf.imes.framework.common.pojo.PageParam;
import com.cf.imes.framework.common.pojo.PageResult;
@@ -96,7 +97,7 @@ public class SmsTemplateController {
@Operation(summary = "发送短信")
@PreAuthorize("@ss.hasPermission('system:sms-template:send-sms')")
public CommonResult<Long> sendSms(@Valid @RequestBody SmsTemplateSendReqVO sendReqVO) {
return success(smsSendService.sendSingleSmsToAdmin(sendReqVO.getMobile(), null,
return success(smsSendService.sendSingleSms(sendReqVO.getMobile(), null, UserTypeEnum.ADMIN.getValue(),
sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams()));
}
@@ -1,30 +0,0 @@
package com.cf.imes.module.system.controller.admin.sms.vo.channel;
import com.cf.imes.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static com.cf.imes.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 短信渠道分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SmsChannelPageReqVO extends PageParam {
@Schema(description = "任务状态", example = "1")
private Integer status;
@Schema(description = "短信签名,模糊匹配", example = "晨丰科技")
private String signature;
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@Schema(description = "创建时间")
private LocalDateTime[] createTime;
}
@@ -1,45 +0,0 @@
package com.cf.imes.module.system.controller.admin.sms.vo.channel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 短信渠道 Response VO")
@Data
public class SmsChannelRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "短信签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "晨丰科技")
@NotNull(message = "短信签名不能为空")
private String signature;
@Schema(description = "渠道编码,参见 SmsChannelEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "YUN_PIAN")
private String code;
@Schema(description = "启用状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "启用状态不能为空")
private Integer status;
@Schema(description = "备注", example = "好吃!")
private String remark;
@Schema(description = "短信 API 的账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "chenfeng")
@NotNull(message = "短信 API 的账号不能为空")
private String apiKey;
@Schema(description = "短信 API 的密钥", example = "yuanma")
private String apiSecret;
@Schema(description = "短信发送回调 URL", example = "https://www.cf.com")
@URL(message = "回调 URL 格式不正确")
private String callbackUrl;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}
@@ -1,42 +0,0 @@
package com.cf.imes.module.system.controller.admin.sms.vo.channel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import jakarta.validation.constraints.NotNull;
@Schema(description = "管理后台 - 短信渠道创建/修改 Request VO")
@Data
public class SmsChannelSaveReqVO {
@Schema(description = "编号", example = "1024")
private Long id;
@Schema(description = "短信签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "晨丰科技")
@NotNull(message = "短信签名不能为空")
private String signature;
@Schema(description = "渠道编码,参见 SmsChannelEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "YUN_PIAN")
@NotNull(message = "渠道编码不能为空")
private String code;
@Schema(description = "启用状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "启用状态不能为空")
private Integer status;
@Schema(description = "备注", example = "好吃!")
private String remark;
@Schema(description = "短信 API 的账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "chenfeng")
@NotNull(message = "短信 API 的账号不能为空")
private String apiKey;
@Schema(description = "短信 API 的密钥", example = "yuanma")
private String apiSecret;
@Schema(description = "短信发送回调 URL", example = "http://www.cf.com")
@URL(message = "回调 URL 格式不正确")
private String callbackUrl;
}
@@ -1,19 +0,0 @@
package com.cf.imes.module.system.controller.admin.sms.vo.channel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - 短信渠道精简 Response VO")
@Data
public class SmsChannelSimpleRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "短信签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "晨丰科技")
private String signature;
@Schema(description = "渠道编码,参见 SmsChannelEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "YUN_PIAN")
private String code;
}
@@ -3,8 +3,8 @@ package com.cf.imes.module.system.controller.admin.sms.vo.template;
import com.cf.imes.module.system.validation.common.CommonStatus;
import com.cf.imes.module.system.validation.sms.SmsTemplateTypeValid;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import jakarta.validation.constraints.NotNull;
@@ -27,26 +27,26 @@ public class SmsTemplateSaveReqVO {
@Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01")
@NotNull(message = "模板编码不能为空")
@Length(min = 1, max = 64, message = "模板编码长度不能超过64个字符")
@Size(min = 1, max = 64, message = "模板编码长度不能超过64个字符")
private String code;
@Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "chenfeng")
@NotNull(message = "模板名称不能为空")
@Length(min = 1, max = 64, message = "模板名称长度不能超过64个字符")
@Size(min = 1, max = 64, message = "模板名称长度不能超过64个字符")
private String name;
@Schema(description = "模板内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,{name}。你长的太{like}啦!")
@NotNull(message = "模板内容不能为空")
@Length(min = 1, max = 255, message = "模板内容长度不能超过255个字符")
@Size(min = 1, max = 255, message = "模板内容长度不能超过255个字符")
private String content;
@Schema(description = "备注", example = "哈哈哈")
@Length(max = 255, message = "备注长度不能超过255个字符")
@Size(max = 255, message = "备注长度不能超过255个字符")
private String remark;
@Schema(description = "短信 API 的模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4383920")
@NotNull(message = "短信 API 的模板编号不能为空")
@Length(min = 1, max = 64, message = "短信 API 的模板编号长度不能超过64个字符")
@Size(min = 1, max = 64, message = "短信 API 的模板编号长度不能超过64个字符")
private String apiTemplateId;
}
@@ -1,62 +0,0 @@
package com.cf.imes.module.system.dal.dataobject.sms;
import com.cf.imes.framework.common.enums.CommonStatusEnum;
import com.cf.imes.framework.mybatis.core.dataobject.BaseDO;
import com.cf.imes.module.system.framework.sms.core.enums.SmsChannelEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* 短信渠道 DO
*
* @author zzf
* @since 2021-01-25
*/
@TableName(value = "system_sms_channel", autoResultMap = true)
@KeySequence("system_sms_channel_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class SmsChannelDO extends BaseDO {
/**
* 渠道编号
*/
private Long id;
/**
* 短信签名
*/
private String signature;
/**
* 渠道编码
*
* 枚举 {@link SmsChannelEnum}
*/
private String code;
/**
* 启用状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
/**
* 备注
*/
private String remark;
/**
* 短信 API 的账号
*/
private String apiKey;
/**
* 短信 API 的密钥
*/
private String apiSecret;
/**
* 短信发送回调 URL
*/
private String callbackUrl;
}
@@ -1,65 +0,0 @@
package com.cf.imes.module.system.dal.dataobject.sms;
import com.cf.imes.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
* 手机验证码 DO
*
* idx_mobile 索引:基于 {@link #mobile} 字段
*
* @author 晨丰科技
*/
@TableName("system_sms_code")
@KeySequence("system_sms_code_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SmsCodeDO extends BaseDO {
/**
* 编号
*/
private Long id;
/**
* 手机号
*/
private String mobile;
/**
* 验证码
*/
private String code;
/**
* 发送场景
*
* 枚举 {@link SmsCodeDO}
*/
private Integer scene;
/**
* 创建 IP
*/
private String createIp;
/**
* 今日发送的第几条
*/
private Integer todayIndex;
/**
* 是否使用
*/
private Boolean used;
/**
* 使用时间
*/
private LocalDateTime usedTime;
/**
* 使用 IP
*/
private String usedIp;
}
@@ -41,13 +41,11 @@ public class SmsLogDO extends BaseDO {
/**
* 短信渠道编号
*
* 关联 {@link SmsChannelDO#getId()}
*/
private Long channelId;
/**
* 短信渠道编码
*
* 冗余 {@link SmsChannelDO#getCode()}
*/
private String channelCode;
@@ -80,13 +80,11 @@ public class SmsTemplateDO extends BaseDO {
/**
* 短信渠道编号
*
* 关联 {@link SmsChannelDO#getId()}
*/
private Long channelId;
/**
* 短信渠道编码
*
* 冗余 {@link SmsChannelDO#getCode()}
*/
private String channelCode;
@@ -1,25 +0,0 @@
package com.cf.imes.module.system.dal.mysql.sms;
import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.framework.mybatis.core.mapper.BaseMapperX;
import com.cf.imes.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.cf.imes.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsChannelDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SmsChannelMapper extends BaseMapperX<SmsChannelDO> {
default PageResult<SmsChannelDO> selectPage(SmsChannelPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<SmsChannelDO>()
.likeIfPresent(SmsChannelDO::getSignature, reqVO.getSignature())
.eqIfPresent(SmsChannelDO::getStatus, reqVO.getStatus())
.betweenIfPresent(SmsChannelDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(SmsChannelDO::getId));
}
default SmsChannelDO selectByCode(String code) {
return selectOne(SmsChannelDO::getCode, code);
}
}
@@ -1,28 +0,0 @@
package com.cf.imes.module.system.dal.mysql.sms;
import com.cf.imes.framework.mybatis.core.mapper.BaseMapperX;
import com.cf.imes.framework.mybatis.core.query.QueryWrapperX;
import com.cf.imes.module.system.dal.dataobject.sms.SmsCodeDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SmsCodeMapper extends BaseMapperX<SmsCodeDO> {
/**
* 获得手机号的最后一个手机验证码
*
* @param mobile 手机号
* @param scene 发送场景,选填
* @param code 验证码 选填
* @return 手机验证码
*/
default SmsCodeDO selectLastByMobile(String mobile, String code, Integer scene) {
return selectOne(new QueryWrapperX<SmsCodeDO>()
.eq("mobile", mobile)
.eqIfPresent("scene", scene)
.eqIfPresent("code", code)
.orderByDesc("id")
.limitN(1));
}
}
@@ -9,14 +9,6 @@ import com.cf.imes.module.system.framework.sms.core.property.SmsProperties;
* @since 2021/1/28 14:01
*/
public interface SmsClientFactory {
/**
* 获得短信 Client
*
* @param channelCode 渠道编码
* @return 短信 Client
*/
SmsClient getSmsClient(String channelCode);
/**
* 获得短信 Client
*
@@ -55,11 +55,6 @@ public class SmsClientFactoryImpl implements SmsClientFactory {
return client;
}
@Override
public SmsClient getSmsClient(String channelCode) {
return channelCodeClients.get(channelCode);
}
private AbstractSmsClient createSmsClient(SmsProperties smsProperties) {
String channel = smsProperties.getChannel();
SmsChannelEnum channelEnum = SmsChannelEnum.getByCode(channel);
@@ -1,13 +1,6 @@
package com.cf.imes.module.system.service.sms;
import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.module.system.framework.sms.core.client.SmsClient;
import com.cf.imes.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO;
import com.cf.imes.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsChannelDO;
import jakarta.validation.Valid;
import java.util.List;
/**
* 短信渠道 Service 接口
@@ -17,67 +10,6 @@ import java.util.List;
*/
public interface SmsChannelService {
/**
* 创建短信渠道
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createSmsChannel(@Valid SmsChannelSaveReqVO createReqVO);
/**
* 更新短信渠道
*
* @param updateReqVO 更新信息
*/
void updateSmsChannel(@Valid SmsChannelSaveReqVO updateReqVO);
/**
* 删除短信渠道
*
* @param id 编号
*/
void deleteSmsChannel(Long id);
/**
* 获得短信渠道
*
* @param id 编号
* @return 短信渠道
*/
SmsChannelDO getSmsChannel(Long id);
/**
* 获得所有短信渠道列表
*
* @return 短信渠道列表
*/
List<SmsChannelDO> getSmsChannelList();
/**
* 获得短信渠道分页
*
* @param pageReqVO 分页查询
* @return 短信渠道分页
*/
PageResult<SmsChannelDO> getSmsChannelPage(SmsChannelPageReqVO pageReqVO);
/**
* 获得短信客户端
*
* @param id 编号
* @return 短信客户端
*/
SmsClient getSmsClient(Long id);
/**
* 获得短信客户端
*
* @param code 编码
* @return 短信客户端
*/
SmsClient getSmsClient(String code);
/**
* 根据配置获得短信客户端
*
@@ -1,28 +1,13 @@
package com.cf.imes.module.system.service.sms;
import cn.hutool.core.text.CharSequenceUtil;
import com.cf.imes.framework.common.exception.util.ServiceExceptionUtil;
import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.framework.common.util.object.BeanUtils;
import com.cf.imes.module.system.framework.sms.core.client.SmsClient;
import com.cf.imes.module.system.framework.sms.core.client.SmsClientFactory;
import com.cf.imes.module.system.framework.sms.core.property.SmsProperties;
import com.cf.imes.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO;
import com.cf.imes.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsChannelDO;
import com.cf.imes.module.system.dal.mysql.sms.SmsChannelMapper;
import com.cf.imes.module.system.enums.ErrorCodeConstants;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import java.time.Duration;
import java.util.List;
import static com.cf.imes.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
/**
* 短信渠道 Service 实现类
@@ -36,123 +21,9 @@ public class SmsChannelServiceImpl implements SmsChannelService {
@Resource
private SmsClientFactory smsClientFactory;
@Resource
private SmsChannelMapper smsChannelMapper;
@Resource
private SmsTemplateService smsTemplateService;
@Resource
private SmsProperties smsProperties;
/**
* {@link SmsClient} 缓存,通过它异步刷新 smsClientFactory
*/
@Getter
private final LoadingCache<Long, SmsClient> idClientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L),
new CacheLoader<>() {
@Override
public SmsClient load(Long id) {
return smsClientFactory.getSmsClient(smsProperties);
}
});
/**
* {@link SmsClient} 缓存,通过它异步刷新 smsClientFactory
*/
@Getter
private final LoadingCache<String, SmsClient> codeClientCache = buildAsyncReloadingCache(Duration.ofSeconds(60L),
new CacheLoader<>() {
@Override
public SmsClient load(String code) {
return smsClientFactory.getSmsClient(smsProperties);
}
});
@Override
public Long createSmsChannel(SmsChannelSaveReqVO createReqVO) {
SmsChannelDO channel = BeanUtils.toBean(createReqVO, SmsChannelDO.class);
smsChannelMapper.insert(channel);
return channel.getId();
}
@Override
public void updateSmsChannel(SmsChannelSaveReqVO updateReqVO) {
// 校验存在
SmsChannelDO channel = validateSmsChannelExists(updateReqVO.getId());
// 更新
SmsChannelDO updateObj = BeanUtils.toBean(updateReqVO, SmsChannelDO.class);
smsChannelMapper.updateById(updateObj);
// 清空缓存
clearCache(updateReqVO.getId(), channel.getCode());
}
@Override
public void deleteSmsChannel(Long id) {
// 校验存在
SmsChannelDO channel = validateSmsChannelExists(id);
// 校验是否有在使用该账号的模版
if (smsTemplateService.getSmsTemplateCountByChannelId(id) > 0) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN);
}
// 删除
smsChannelMapper.deleteById(id);
// 清空缓存
clearCache(id, channel.getCode());
}
/**
* 清空指定渠道编号的缓存
*
* @param id 渠道编号
* @param code 渠道编码
*/
private void clearCache(Long id, String code) {
idClientCache.invalidate(id);
if (CharSequenceUtil.isNotEmpty(code)) {
codeClientCache.invalidate(code);
}
}
private SmsChannelDO validateSmsChannelExists(Long id) {
SmsChannelDO channel = smsChannelMapper.selectById(id);
if (channel == null) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS);
}
return channel;
}
@Override
public SmsChannelDO getSmsChannel(Long id) {
return smsChannelMapper.selectById(id);
}
@Override
public List<SmsChannelDO> getSmsChannelList() {
return smsChannelMapper.selectList();
}
@Override
public PageResult<SmsChannelDO> getSmsChannelPage(SmsChannelPageReqVO pageReqVO) {
return smsChannelMapper.selectPage(pageReqVO);
}
@Override
public SmsClient getSmsClient(Long id) {
return idClientCache.getUnchecked(id);
}
@Override
public SmsClient getSmsClient(String code) {
return smsClientFactory.getSmsClient(smsProperties);
}
@Override
public SmsClient getSmsClient() {
return smsClientFactory.getSmsClient(smsProperties);
@@ -3,7 +3,6 @@ package com.cf.imes.module.system.service.sms;
import com.cf.imes.framework.common.exception.ServiceException;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeValidateReqDTO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginSmsCheckReqVO;
import jakarta.validation.Valid;
@@ -31,13 +30,6 @@ public interface SmsCodeService {
*/
void useSmsCode(@Valid SmsCodeUseReqDTO reqDTO);
/**
* 检查验证码是否有效
*
* @param reqDTO 校验请求
*/
void validateSmsCode(@Valid SmsCodeValidateReqDTO reqDTO);
/**
* 检查验证码是否有效
*
@@ -1,6 +1,5 @@
package com.cf.imes.module.system.service.sms;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
@@ -10,11 +9,8 @@ import com.cf.imes.framework.security.core.LoginUser;
import com.cf.imes.framework.security.core.util.SecurityFrameworkUtils;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeValidateReqDTO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginSmsCheckReqVO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsCodeDO;
import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO;
import com.cf.imes.module.system.dal.mysql.sms.SmsCodeMapper;
import com.cf.imes.module.system.dal.redis.RedisKeyConstants;
import com.cf.imes.module.system.enums.sms.SmsSceneEnum;
import com.cf.imes.module.system.framework.sms.config.SmsCodeProperties;
@@ -27,7 +23,6 @@ import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
@@ -45,9 +40,6 @@ public class SmsCodeServiceImpl implements SmsCodeService {
@Resource
private SmsCodeProperties smsCodeProperties;
@Resource
private SmsCodeMapper smsCodeMapper;
@Resource
private SmsSendService smsSendService;
@@ -215,28 +207,4 @@ public class SmsCodeServiceImpl implements SmsCodeService {
}
}
@Override
public void validateSmsCode(SmsCodeValidateReqDTO reqDTO) {
validateSmsCode0(reqDTO.getMobile(), reqDTO.getCode(), reqDTO.getScene());
}
private SmsCodeDO validateSmsCode0(String mobile, String code, Integer scene) {
// 校验验证码
SmsCodeDO lastSmsCode = smsCodeMapper.selectLastByMobile(mobile, code, scene);
// 若验证码不存在,抛出异常
if (lastSmsCode == null) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CODE_NOT_FOUND);
}
// 超过时间
if (LocalDateTimeUtil.between(lastSmsCode.getCreateTime(), LocalDateTime.now()).toMillis()
>= smsCodeProperties.getExpireTimes().toMillis()) { // 验证码已过期
throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CODE_EXPIRED);
}
// 判断验证码是否已被使用
if (Boolean.TRUE.equals(lastSmsCode.getUsed())) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CODE_USED);
}
return lastSmsCode;
}
}
@@ -32,8 +32,7 @@ public class SmsLogServiceImpl implements SmsLogService {
SmsTemplateDO template, String templateContent, Map<String, Object> templateParams) {
SmsLogDO.SmsLogDOBuilder logBuilder = SmsLogDO.builder();
// 根据是否要发送,设置状态
logBuilder.sendStatus(Objects.equals(isSend, true) ? SmsSendStatusEnum.INIT.getStatus()
: SmsSendStatusEnum.IGNORE.getStatus());
logBuilder.sendStatus(SmsSendStatusEnum.INIT.getStatus());
// 设置手机相关字段
logBuilder.mobile(mobile).userId(userId).userType(userType);
// 设置模板相关字段
@@ -11,35 +11,6 @@ import java.util.Map;
* @author 晨丰科技
*/
public interface SmsSendService {
/**
* 发送单条短信给管理后台的用户
*
* 在 mobile 为空时,使用 userId 加载对应管理员的手机号
*
* @param mobile 手机号
* @param userId 用户编号
* @param templateCode 短信模板编号
* @param templateParams 短信模板参数
* @return 发送日志编号
*/
Long sendSingleSmsToAdmin(String mobile, Long userId,
String templateCode, Map<String, Object> templateParams);
/**
* 发送单条短信给用户 APP 的用户
*
* 在 mobile 为空时,使用 userId 加载对应会员的手机号
*
* @param mobile 手机号
* @param userId 用户编号
* @param templateCode 短信模板编号
* @param templateParams 短信模板参数
* @return 发送日志编号
*/
Long sendSingleSmsToMember(String mobile, Long userId,
String templateCode, Map<String, Object> templateParams);
/**
* 发送单条短信给用户
*
@@ -5,22 +5,20 @@ import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.lang.Assert;
import com.cf.imes.framework.common.core.KeyValue;
import com.cf.imes.framework.common.enums.CommonStatusEnum;
import com.cf.imes.framework.common.enums.UserTypeEnum;
import com.cf.imes.framework.common.exception.util.ServiceExceptionUtil;
import com.cf.imes.module.system.dal.redis.RedisKeyConstants;
import com.cf.imes.module.system.framework.sms.config.SmsCodeProperties;
import com.cf.imes.module.system.framework.sms.core.client.SmsClient;
import com.cf.imes.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
import com.cf.imes.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsTemplateDO;
import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO;
import com.cf.imes.module.system.mq.message.sms.SmsSendMessage;
import com.cf.imes.module.system.mq.producer.sms.SmsProducer;
import com.cf.imes.module.system.service.member.MemberService;
import com.cf.imes.module.system.service.sms.handler.SmsSendAfterSendHandler;
import com.cf.imes.module.system.service.user.AdminUserService;
import com.cf.imes.module.system.enums.ErrorCodeConstants;
import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
@@ -37,10 +35,6 @@ import java.util.stream.Collectors;
@Slf4j
public class SmsSendServiceImpl implements SmsSendService {
@Resource
private AdminUserService adminUserService;
@Resource
private MemberService memberService;
@Resource
private SmsChannelService smsChannelService;
@Resource
@@ -52,30 +46,10 @@ public class SmsSendServiceImpl implements SmsSendService {
private SmsProducer smsProducer;
@Resource
private List<SmsSendAfterSendHandler> smsSendAfterSendHandlers;
private RedisTemplate redisTemplate;
@Override
public Long sendSingleSmsToAdmin(String mobile, Long userId, String templateCode, Map<String, Object> templateParams) {
// 如果 mobile 为空,则加载用户编号对应的手机号
if (StringUtils.isEmpty(mobile)) {
AdminUserDO user = adminUserService.getUser(userId);
if (user != null) {
mobile = user.getMobile();
}
}
// 执行发送
return sendSingleSms(mobile, userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams);
}
@Override
public Long sendSingleSmsToMember(String mobile, Long userId, String templateCode, Map<String, Object> templateParams) {
// 如果 mobile 为空,则加载用户编号对应的手机号
if (StringUtils.isEmpty(mobile)) {
mobile = memberService.getMemberUserMobile(userId);
}
// 执行发送
return sendSingleSms(mobile, userId, UserTypeEnum.MEMBER.getValue(), templateCode, templateParams);
}
@Resource
private SmsCodeProperties smsCodeProperties;
@Override
public Long sendSingleSms(String mobile, Long userId, Integer userType,
@@ -95,7 +69,9 @@ public class SmsSendServiceImpl implements SmsSendService {
// 发送 MQ 消息,异步执行发送短信
if (isSend) {
smsProducer.sendSmsSendMessage(sendLogId, mobile, template, newTemplateParams);
SmsSendMessage message = new SmsSendMessage().setLogId(sendLogId).setMobile(mobile);
message.setChannelId(template.getChannelId()).setApiTemplateId(template.getApiTemplateId()).setTemplateType(template.getType()).setTemplateParams(newTemplateParams);
doSendSms(message);
}
return sendLogId;
}
@@ -153,9 +129,11 @@ public class SmsSendServiceImpl implements SmsSendService {
message.getApiTemplateId(), templateParams);
channelCode = sendResponse.getChannelCode();
// 发送成功后操作
for (SmsSendAfterSendHandler afterSendHandler : smsSendAfterSendHandlers) {
if (afterSendHandler.checkTemplateType(message.getTemplateType())) {
afterSendHandler.afterSend(sendResponse, templateParams);
if (sendResponse.getSuccess()) {
for (KeyValue<String, Object> keyValue : templateParams) {
if ("code".equals(keyValue.getKey())) {
redisTemplate.opsForValue().set(String.format(RedisKeyConstants.SMS_CAPTCHA_VERIFICATION, sendResponse.getMobile()), keyValue.getValue(), smsCodeProperties.getExpireTimes());
}
}
}
smsLogService.updateSmsSendResult(message.getLogId(), sendResponse.getSuccess(),
@@ -171,7 +149,7 @@ public class SmsSendServiceImpl implements SmsSendService {
@Override
public void receiveSmsStatus(String channelCode, String text) throws Throwable {
// 获得渠道对应的 SmsClient 客户端
SmsClient smsClient = smsChannelService.getSmsClient(channelCode);
SmsClient smsClient = smsChannelService.getSmsClient();
Assert.notNull(smsClient, "短信客户端({}) 不存在", channelCode);
// 解析内容
List<SmsReceiveRespDTO> receiveResults = smsClient.parseSmsReceiveStatus(text);
@@ -5,7 +5,6 @@ import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import com.cf.imes.framework.common.enums.CommonStatusEnum;
import com.cf.imes.framework.common.exception.util.ServiceExceptionUtil;
import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.framework.common.util.object.BeanUtils;
@@ -14,7 +13,6 @@ import com.cf.imes.module.system.framework.sms.core.client.dto.SmsTemplateRespDT
import com.cf.imes.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import com.cf.imes.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO;
import com.cf.imes.module.system.controller.admin.sms.vo.template.SmsTemplateSaveReqVO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsChannelDO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsTemplateDO;
import com.cf.imes.module.system.dal.mysql.sms.SmsTemplateMapper;
import com.cf.imes.module.system.dal.redis.RedisKeyConstants;
@@ -122,18 +120,6 @@ public class SmsTemplateServiceImpl implements SmsTemplateService {
return smsTemplateMapper.selectCountByChannelId(channelId);
}
@VisibleForTesting
public SmsChannelDO validateSmsChannel(Long channelId) {
SmsChannelDO channelDO = smsChannelService.getSmsChannel(channelId);
if (channelDO == null) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS);
}
if (CommonStatusEnum.isDisable(channelDO.getStatus())) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CHANNEL_DISABLE);
}
return channelDO;
}
@VisibleForTesting
public void validateSmsTemplateCodeDuplicate(Long id, String code) {
SmsTemplateDO template = smsTemplateMapper.selectByCode(code);
@@ -154,6 +140,7 @@ public class SmsTemplateServiceImpl implements SmsTemplateService {
*
* @param apiTemplateId API 模板编号
*/
@VisibleForTesting
void validateApiTemplate(String apiTemplateId) {
// 获得短信模板
SmsClient smsClient = smsChannelService.getSmsClient();
@@ -1,25 +0,0 @@
package com.cf.imes.module.system.service.sms.handler;
import com.cf.imes.framework.common.core.KeyValue;
import com.cf.imes.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
import java.util.List;
/**
* 短信发送后处理器接口
*
* @author Gqr
* @since 2024/8/12 18:42
*/
public interface SmsSendAfterSendHandler {
/**
* 校验消息模板类型
*
* @param templateType
* @return
*/
boolean checkTemplateType(Integer templateType);
void afterSend(SmsSendRespDTO sendResponse, List<KeyValue<String, Object>> params);
}
@@ -1,46 +0,0 @@
package com.cf.imes.module.system.service.sms.handler.impl;
import cn.hutool.core.util.ObjectUtil;
import com.cf.imes.framework.common.core.KeyValue;
import com.cf.imes.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
import com.cf.imes.module.system.dal.redis.RedisKeyConstants;
import com.cf.imes.module.system.enums.sms.SmsTemplateTypeEnum;
import com.cf.imes.module.system.framework.sms.config.SmsCodeProperties;
import com.cf.imes.module.system.service.sms.handler.SmsSendAfterSendHandler;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import java.util.List;
/**
* @author Gqr
* @since 2024/8/12 18:49
*/
@Service
public class SmsSendAfterSendHandlerImpl implements SmsSendAfterSendHandler {
@Resource
private RedisTemplate redisTemplate;
@Resource
private SmsCodeProperties smsCodeProperties;
@Override
public boolean checkTemplateType(Integer templateType) {
SmsTemplateTypeEnum smsTemplateTypeEnum = SmsTemplateTypeEnum.valueOf(templateType);
return ObjectUtil.isNotNull(smsTemplateTypeEnum);
}
@Override
public void afterSend(SmsSendRespDTO sendResponse, List<KeyValue<String, Object>> params) {
// 发送成功把验证码存入redis
if (sendResponse.getSuccess()) {
for (KeyValue<String, Object> keyValue : params) {
if ("code".equals(keyValue.getKey())) {
redisTemplate.opsForValue().set(String.format(RedisKeyConstants.SMS_CAPTCHA_VERIFICATION, sendResponse.getMobile()), keyValue.getValue(), smsCodeProperties.getExpireTimes());
}
}
}
}
}
@@ -133,7 +133,6 @@ logging:
# 配置自己写的 MyBatis Mapper 打印日志
com.cf.imes.module.system.dal.mysql: debug
com.cf.imes.module.system.dal.mysql.sensitiveword.SensitiveWordMapper: INFO # 配置 SensitiveWordMapper 的日志级别为 info
com.cf.imes.module.system.dal.mysql.sms.SmsChannelMapper: INFO # 配置 SmsChannelMapper 的日志级别为 info
org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO :先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示
--- #################### 微信公众号、小程序相关配置 ####################
@@ -1,6 +1,5 @@
package com.cf.imes.module.system.controller.admin.dept;
import com.cf.imes.framework.security.test.WithMockLoginUser;
import com.cf.imes.module.system.controller.admin.dept.vo.dept.DeptListReqVO;
import com.cf.imes.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO;
import com.cf.imes.module.system.dal.dataobject.dept.DeptDO;
@@ -59,7 +58,6 @@ public class DeptControllerTest {
}
@Test
@WithMockLoginUser(isSuperAdmin = true)
void testCreateDept() throws Exception {
when(deptService.createDept(any())).thenReturn(123L);
@@ -0,0 +1,88 @@
package com.cf.imes.module.system.controller.admin.sms;
import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsLogDO;
import com.cf.imes.module.system.service.sms.SmsLogService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Collections;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Gqr
* @since 2025/10/10 15:37
*/
@WebMvcTest(controllers = SmsLogController.class, excludeAutoConfiguration = SecurityAutoConfiguration.class)
public class SmsLogControllerTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean
private SmsLogService smsLogService;
@Autowired
private ObjectMapper objectMapper;
/**
* 测试分页查询接口
*/
@Test
void testGetSmsLogPage_success() throws Exception {
// mock service 层返回分页结果
SmsLogDO logDO = new SmsLogDO();
logDO.setId(1L);
logDO.setMobile("13800000000");
logDO.setSendStatus(1);
PageResult<SmsLogDO> pageResult = new PageResult<>(Collections.singletonList(logDO), 1L);
Mockito.when(smsLogService.getSmsLogPage(any())).thenReturn(pageResult);
// 构造查询参数
SmsLogPageReqVO reqVO = new SmsLogPageReqVO();
reqVO.setPageNo(1);
reqVO.setPageSize(10);
// 执行请求
mockMvc.perform(get("/system/sms-log/page")
.contentType(MediaType.APPLICATION_JSON)
.param("pageNo", "1")
.param("pageSize", "10"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.list[0].mobile").value("13800000000"));
}
/**
* 测试导出 Excel 接口
*/
@Test
void testExportSmsLogExcel_success() throws Exception {
// mock service 层返回数据
SmsLogDO logDO = new SmsLogDO();
logDO.setId(2L);
logDO.setMobile("13900000000");
PageResult<SmsLogDO> pageResult = new PageResult<>(List.of(logDO), 1L);
Mockito.when(smsLogService.getSmsLogPage(any())).thenReturn(pageResult);
// 执行请求
mockMvc.perform(get("/system/sms-log/export")
.contentType(MediaType.APPLICATION_JSON)
.param("pageNo", "1")
.param("pageSize", "10"))
.andExpect(status().isOk())
// 校验响应头是下载 Excel 文件
.andExpect(header().string("Content-Disposition", org.hamcrest.Matchers.containsString("attachment")));
}
}
@@ -0,0 +1,196 @@
package com.cf.imes.module.system.controller.admin.sms;
import com.cf.imes.framework.common.enums.CommonStatusEnum;
import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.module.system.controller.admin.sms.vo.template.SmsTemplateSaveReqVO;
import com.cf.imes.module.system.controller.admin.sms.vo.template.SmsTemplateSendReqVO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsTemplateDO;
import com.cf.imes.module.system.enums.sms.SmsTemplateTypeEnum;
import com.cf.imes.module.system.service.sms.SmsSendService;
import com.cf.imes.module.system.service.sms.SmsTemplateService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
import java.util.Map;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Gqr
* @since 2025/10/11 16:26
*/
@WebMvcTest(controllers = SmsTemplateController.class, excludeAutoConfiguration = SecurityAutoConfiguration.class)
public class SmsTemplateControllerTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean
private SmsTemplateService smsTemplateService;
@MockitoBean
private SmsSendService smsSendService;
@Autowired
private ObjectMapper objectMapper;
// ========== create ==========
@Test
void testCreateSmsTemplate() throws Exception {
SmsTemplateSaveReqVO req = new SmsTemplateSaveReqVO();
req.setCode("REGISTER_CODE");
req.setName("注册验证码");
req.setContent("您的验证码是{code}");
req.setType(SmsTemplateTypeEnum.NOTICE.getType());
req.setStatus(CommonStatusEnum.ENABLE.getStatus());
req.setApiTemplateId("TEMPLATE_001");
when(smsTemplateService.createSmsTemplate(any())).thenReturn(1L);
mockMvc.perform(post("/system/sms-template/create")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data").value(1));
verify(smsTemplateService, times(1)).createSmsTemplate(any());
}
// ========== update ==========
@Test
void testUpdateSmsTemplate() throws Exception {
SmsTemplateSaveReqVO req = new SmsTemplateSaveReqVO();
req.setId(100L);
req.setCode("REGISTER_CODE");
req.setName("注册验证码");
req.setContent("您的验证码是{code}");
req.setType(SmsTemplateTypeEnum.NOTICE.getType());
req.setStatus(CommonStatusEnum.ENABLE.getStatus());
req.setApiTemplateId("TEMPLATE_001");
doNothing().when(smsTemplateService).updateSmsTemplate(any());
mockMvc.perform(put("/system/sms-template/update")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data").value(true));
verify(smsTemplateService, times(1)).updateSmsTemplate(any());
}
// ========== delete ==========
@Test
void testDeleteSmsTemplate() throws Exception {
doNothing().when(smsTemplateService).deleteSmsTemplate(1L);
mockMvc.perform(delete("/system/sms-template/delete")
.param("id", "1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data").value(true));
verify(smsTemplateService, times(1)).deleteSmsTemplate(1L);
}
// ========== get ==========
@Test
void testGetSmsTemplate() throws Exception {
SmsTemplateDO templateDO = new SmsTemplateDO();
templateDO.setId(1L);
templateDO.setCode("REGISTER_CODE");
templateDO.setName("注册验证码");
when(smsTemplateService.getSmsTemplate(1L)).thenReturn(templateDO);
mockMvc.perform(get("/system/sms-template/get")
.param("id", "1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.code").value("REGISTER_CODE"))
.andExpect(jsonPath("$.data.name").value("注册验证码"));
verify(smsTemplateService, times(1)).getSmsTemplate(1L);
}
// ========== page ==========
@Test
void testGetSmsTemplatePage() throws Exception {
SmsTemplateDO templateDO = new SmsTemplateDO();
templateDO.setCode("REGISTER_CODE");
PageResult<SmsTemplateDO> pageResult = new PageResult<>(List.of(templateDO), 1L);
when(smsTemplateService.getSmsTemplatePage(any())).thenReturn(pageResult);
mockMvc.perform(get("/system/sms-template/page")
.param("pageNo", "1")
.param("pageSize", "10"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.list[0].code").value("REGISTER_CODE"));
verify(smsTemplateService, times(1)).getSmsTemplatePage(any());
}
// ========== send-sms ==========
@Test
void testSendSms() throws Exception {
SmsTemplateSendReqVO sendReqVO = new SmsTemplateSendReqVO();
sendReqVO.setMobile("13800000000");
sendReqVO.setTemplateCode("SMS_CODE");
sendReqVO.setTemplateParams(Map.of("code", "123456"));
when(smsSendService.sendSingleSms(anyString(), isNull(), anyInt(), anyString(), anyMap()))
.thenReturn(123L);
mockMvc.perform(post("/system/sms-template/send-sms")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(sendReqVO)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data").value(123));
verify(smsSendService, times(1)).sendSingleSms(
eq("13800000000"),
isNull(),
anyInt(),
eq("SMS_CODE"),
anyMap()
);
}
// ========== export ==========
@Test
void testExportSmsTemplateExcel() throws Exception {
SmsTemplateDO templateDO = new SmsTemplateDO();
templateDO.setId(1L);
templateDO.setCode("REGISTER_CODE");
templateDO.setName("注册验证码");
PageResult<SmsTemplateDO> pageResult = new PageResult<>(List.of(templateDO), 1L);
when(smsTemplateService.getSmsTemplatePage(any())).thenReturn(pageResult);
mockMvc.perform(get("/system/sms-template/export-excel")
.contentType(MediaType.APPLICATION_JSON)
.param("pageNo", "1")
.param("pageSize", "10"))
.andExpect(status().isOk())
// 校验响应头是下载 Excel 文件
.andExpect(header().string("Content-Disposition", org.hamcrest.Matchers.containsString("attachment")));
}
}
@@ -1,7 +1,6 @@
package com.cf.imes.module.system.service.organ;
import com.cf.imes.framework.common.enums.CommonStatusEnum;
import com.cf.imes.framework.common.exception.ServiceException;
import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.framework.organ.config.OrganProperties;
import com.cf.imes.framework.organ.core.context.OrganContextHolder;
@@ -14,27 +13,25 @@ import com.cf.imes.module.system.dal.dataobject.organ.OrganizationDO;
import com.cf.imes.module.system.dal.dataobject.organ.TenantPackageDO;
import com.cf.imes.module.system.dal.mysql.organ.OrganMapper;
import com.cf.imes.module.system.enums.permission.RoleCodeEnum;
import com.cf.imes.module.system.enums.permission.RoleTypeEnum;
import com.cf.imes.module.system.service.permission.MenuService;
import com.cf.imes.module.system.service.permission.PermissionService;
import com.cf.imes.module.system.service.permission.RoleService;
import com.cf.imes.module.system.service.organ.handler.OrganInfoHandler;
import com.cf.imes.module.system.service.organ.handler.OrganMenuHandler;
import com.cf.imes.module.system.service.user.AdminUserService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import jakarta.annotation.Resource;
import java.time.LocalDateTime;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static com.cf.imes.framework.test.core.util.RandomUtils.*;
import static com.cf.imes.framework.common.util.collection.SetUtils.asSet;
import static com.cf.imes.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
import static com.cf.imes.framework.common.util.date.LocalDateTimeUtils.buildTime;
import static com.cf.imes.framework.common.util.object.ObjectUtils.cloneIgnoreId;
import static com.cf.imes.framework.test.core.util.AssertUtils.assertPojoEquals;
@@ -60,27 +57,27 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
@Resource
private OrganMapper tenantMapper;
@MockBean
@MockitoBean
private OrganProperties tenantProperties;
@MockBean
@MockitoBean
private TenantPackageService tenantPackageService;
@MockBean
@MockitoBean
private AdminUserService userService;
@MockBean
@MockitoBean
private RoleService roleService;
@MockBean
@MockitoBean
private MenuService menuService;
@MockBean
@MockitoBean
private PermissionService permissionService;
@BeforeEach
public void setUp() {
void setUp() {
// 清理组织上下文
OrganContextHolder.clear();
}
@Test
public void testgetOrganIdList() {
void testgetOrganIdList() {
// mock 数据
OrganizationDO tenant = randomPojo(OrganizationDO.class, o -> o.setId(1L));
tenantMapper.insert(tenant);
@@ -91,12 +88,12 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testValidTenant_notExists() {
void testValidTenant_notExists() {
assertServiceException(() -> tenantService.validOrgan(randomLongId()), ORGAN_NOT_EXISTS);
}
@Test
public void testValidTenant_disable() {
void testValidTenant_disable() {
// mock 数据
OrganizationDO tenant = randomPojo(OrganizationDO.class, o -> o.setId(1L).setStatus(CommonStatusEnum.DISABLE.getStatus()));
tenantMapper.insert(tenant);
@@ -106,10 +103,10 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testValidTenant_expired() {
void testValidTenant_expired() {
// mock 数据
OrganizationDO tenant = randomPojo(OrganizationDO.class, o -> o.setId(1L).setStatus(CommonStatusEnum.ENABLE.getStatus())
.setExpireTime(buildTime(2020, 2, 2)));
.setExpireTime(LocalDate.now().minusDays(1)));
tenantMapper.insert(tenant);
// 调用,并断言业务异常
@@ -117,10 +114,10 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testValidTenant_success() {
void testValidTenant_success() {
// mock 数据
OrganizationDO tenant = randomPojo(OrganizationDO.class, o -> o.setId(1L).setStatus(CommonStatusEnum.ENABLE.getStatus())
.setExpireTime(LocalDateTime.now().plusDays(1)));
.setExpireTime(LocalDate.now().plusDays(1)));
tenantMapper.insert(tenant);
// 调用,并断言业务异常
@@ -128,7 +125,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testCreateTenant() {
void testCreateTenant() {
// mock 套餐 100L
TenantPackageDO tenantPackage = randomPojo(TenantPackageDO.class, o -> o.setId(100L));
when(tenantPackageService.validTenantPackage(eq(100L))).thenReturn(tenantPackage);
@@ -175,7 +172,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testUpdateTenant_success() {
void testUpdateTenant_success() {
// mock 数据
OrganizationDO dbTenant = randomPojo(OrganizationDO.class, o -> o.setStatus(randomCommonStatus()));
tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据
@@ -210,7 +207,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testUpdateTenant_notExists() {
void testUpdateTenant_notExists() {
// 准备参数
OrganSaveReqVO reqVO = randomPojo(OrganSaveReqVO.class);
@@ -219,7 +216,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testUpdateTenant_system() {
void testUpdateTenant_system() {
// mock 数据
OrganizationDO dbTenant = randomPojo(OrganizationDO.class, o -> o.setPackageId(OrganizationDO.PACKAGE_ID_SYSTEM));
tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据
@@ -233,7 +230,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testDeleteTenant_success() {
void testDeleteTenant_success() {
// mock 数据
OrganizationDO dbTenant = randomPojo(OrganizationDO.class,
o -> o.setStatus(randomCommonStatus()));
@@ -248,7 +245,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testDeleteTenant_notExists() {
void testDeleteTenant_notExists() {
// 准备参数
Long id = randomLongId();
@@ -257,7 +254,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testDeleteTenant_system() {
void testDeleteTenant_system() {
// mock 数据
OrganizationDO dbTenant = randomPojo(OrganizationDO.class, o -> o.setPackageId(OrganizationDO.PACKAGE_ID_SYSTEM));
tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据
@@ -269,7 +266,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testGetTenant() {
void testGetTenant() {
// mock 数据
OrganizationDO dbTenant = randomPojo(OrganizationDO.class);
tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据
@@ -283,7 +280,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testGetTenantPage() {
void testGetTenantPage() {
// mock 数据
OrganizationDO dbTenant = randomPojo(OrganizationDO.class, o -> { // 等会查询到
o.setName("晨丰科技");
@@ -319,7 +316,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testGetTenantByName() {
void testGetTenantByName() {
// mock 数据
OrganizationDO dbTenant = randomPojo(OrganizationDO.class, o -> o.setName("晨丰"));
tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据
@@ -331,7 +328,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testGetTenantByWebsite() {
void testGetTenantByWebsite() {
// mock 数据
OrganizationDO dbTenant = randomPojo(OrganizationDO.class, o -> o.setWebsite("https://www.cf.com"));
tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据
@@ -343,7 +340,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testGetTenantListByPackageId() {
void testGetTenantListByPackageId() {
// mock 数据
OrganizationDO dbTenant1 = randomPojo(OrganizationDO.class, o -> o.setPackageId(1L));
tenantMapper.insert(dbTenant1);// @Sql: 先插入出一条存在的数据
@@ -357,7 +354,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testGetTenantCountByPackageId() {
void testGetTenantCountByPackageId() {
// mock 数据
OrganizationDO dbTenant1 = randomPojo(OrganizationDO.class, o -> o.setPackageId(1L));
tenantMapper.insert(dbTenant1);// @Sql: 先插入出一条存在的数据
@@ -370,7 +367,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testHandleTenantInfo_disable() {
void testHandleTenantInfo_disable() {
// 准备参数
OrganInfoHandler handler = mock(OrganInfoHandler.class);
// mock 禁用
@@ -383,7 +380,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testHandleTenantInfo_success() {
void testHandleTenantInfo_success() {
// 准备参数
OrganInfoHandler handler = mock(OrganInfoHandler.class);
// mock 未禁用
@@ -403,7 +400,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testHandleTenantMenu_disable() {
void testHandleTenantMenu_disable() {
// 准备参数
OrganMenuHandler handler = mock(OrganMenuHandler.class);
// mock 禁用
@@ -416,7 +413,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test // 系统组织的情况
public void testHandleTenantMenu_system() {
void testHandleTenantMenu_system() {
// 准备参数
OrganMenuHandler handler = mock(OrganMenuHandler.class);
// mock 未禁用
@@ -436,7 +433,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test // 普通组织的情况
public void testHandleTenantMenu_normal() {
void testHandleTenantMenu_normal() {
// 准备参数
OrganMenuHandler handler = mock(OrganMenuHandler.class);
// mock 未禁用
@@ -1,229 +1,41 @@
package com.cf.imes.module.system.service.sms;
import com.cf.imes.framework.common.enums.CommonStatusEnum;
import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.framework.common.util.object.BeanUtils;
import com.cf.imes.module.system.framework.sms.config.ChenfengSmsAutoConfiguration;
import com.cf.imes.framework.test.core.ut.BaseDbUnitTest;
import com.cf.imes.module.system.framework.sms.core.client.SmsClient;
import com.cf.imes.module.system.framework.sms.core.client.SmsClientFactory;
import com.cf.imes.module.system.framework.sms.core.client.impl.aliyun.AliyunSmsClient;
import com.cf.imes.module.system.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient;
import com.cf.imes.module.system.framework.sms.core.enums.SmsChannelEnum;
import com.cf.imes.module.system.framework.sms.core.property.SmsChannelProperties;
import com.cf.imes.framework.test.core.ut.BaseDbUnitTest;
import com.cf.imes.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO;
import com.cf.imes.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsChannelDO;
import com.cf.imes.module.system.dal.mysql.sms.SmsChannelMapper;
import com.cf.imes.module.system.framework.sms.core.property.SmsProperties;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import jakarta.annotation.Resource;
import java.util.List;
import static com.cf.imes.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
import static com.cf.imes.framework.common.util.date.LocalDateTimeUtils.buildTime;
import static com.cf.imes.framework.common.util.object.ObjectUtils.cloneIgnoreId;
import static com.cf.imes.framework.test.core.util.AssertUtils.assertPojoEquals;
import static com.cf.imes.framework.test.core.util.AssertUtils.assertServiceException;
import static com.cf.imes.framework.test.core.util.RandomUtils.*;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
@Import(SmsChannelServiceImpl.class)
@Import({SmsChannelServiceImpl.class, ChenfengSmsAutoConfiguration.class})
public class SmsChannelServiceTest extends BaseDbUnitTest {
@Resource
private SmsChannelServiceImpl smsChannelService;
@Resource
private SmsChannelMapper smsChannelMapper;
private SmsProperties smsProperties; // 真实 Bean
@MockBean
private SmsClientFactory smsClientFactory;
@MockBean
private SmsTemplateService smsTemplateService;
@Resource
private SmsClientFactory smsClientFactory; // 真实 Bean
@Test
public void testCreateSmsChannel_success() {
// 准备参数
SmsChannelSaveReqVO reqVO = randomPojo(SmsChannelSaveReqVO.class, o -> o.setStatus(randomCommonStatus()))
.setId(null); // 防止 id 被赋值
void testGetSmsClient_aliyun() {
smsProperties.setChannel(SmsChannelEnum.ALIYUN.getCode());
SmsClient aliyunClient = smsChannelService.getSmsClient();
assertThat(aliyunClient).isInstanceOf(AliyunSmsClient.class);
// 调用
Long smsChannelId = smsChannelService.createSmsChannel(reqVO);
// 断言
assertNotNull(smsChannelId);
// 校验记录的属性是否正确
SmsChannelDO smsChannel = smsChannelMapper.selectById(smsChannelId);
assertPojoEquals(reqVO, smsChannel, "id");
// 断言 cache
assertNull(smsChannelService.getIdClientCache().getIfPresent(smsChannel.getId()));
assertNull(smsChannelService.getCodeClientCache().getIfPresent(smsChannel.getCode()));
smsProperties.setChannel(SmsChannelEnum.DEBUG_DING_TALK.getCode());
SmsClient dingDingClient = smsChannelService.getSmsClient();
assertThat(dingDingClient).isInstanceOf(DebugDingTalkSmsClient.class);
}
@Test
public void testUpdateSmsChannel_success() {
// mock 数据
SmsChannelDO dbSmsChannel = randomPojo(SmsChannelDO.class);
smsChannelMapper.insert(dbSmsChannel);// @Sql: 先插入出一条存在的数据
// 准备参数
SmsChannelSaveReqVO reqVO = randomPojo(SmsChannelSaveReqVO.class, o -> {
o.setId(dbSmsChannel.getId()); // 设置更新的 ID
o.setStatus(randomCommonStatus());
o.setCallbackUrl(randomString());
});
// 调用
smsChannelService.updateSmsChannel(reqVO);
// 校验是否更新正确
SmsChannelDO smsChannel = smsChannelMapper.selectById(reqVO.getId()); // 获取最新的
assertPojoEquals(reqVO, smsChannel);
// 断言 cache
assertNull(smsChannelService.getIdClientCache().getIfPresent(smsChannel.getId()));
assertNull(smsChannelService.getCodeClientCache().getIfPresent(smsChannel.getCode()));
}
@Test
public void testUpdateSmsChannel_notExists() {
// 准备参数
SmsChannelSaveReqVO reqVO = randomPojo(SmsChannelSaveReqVO.class);
// 调用, 并断言异常
assertServiceException(() -> smsChannelService.updateSmsChannel(reqVO), SMS_CHANNEL_NOT_EXISTS);
}
@Test
public void testDeleteSmsChannel_success() {
// mock 数据
SmsChannelDO dbSmsChannel = randomPojo(SmsChannelDO.class);
smsChannelMapper.insert(dbSmsChannel);// @Sql: 先插入出一条存在的数据
// 准备参数
Long id = dbSmsChannel.getId();
// 调用
smsChannelService.deleteSmsChannel(id);
// 校验数据不存在了
assertNull(smsChannelMapper.selectById(id));
// 断言 cache
assertNull(smsChannelService.getIdClientCache().getIfPresent(dbSmsChannel.getId()));
assertNull(smsChannelService.getCodeClientCache().getIfPresent(dbSmsChannel.getCode()));
}
@Test
public void testDeleteSmsChannel_notExists() {
// 准备参数
Long id = randomLongId();
// 调用, 并断言异常
assertServiceException(() -> smsChannelService.deleteSmsChannel(id), SMS_CHANNEL_NOT_EXISTS);
}
@Test
public void testDeleteSmsChannel_hasChildren() {
// mock 数据
SmsChannelDO dbSmsChannel = randomPojo(SmsChannelDO.class);
smsChannelMapper.insert(dbSmsChannel);// @Sql: 先插入出一条存在的数据
// 准备参数
Long id = dbSmsChannel.getId();
// mock 方法
when(smsTemplateService.getSmsTemplateCountByChannelId(eq(id))).thenReturn(10L);
// 调用, 并断言异常
assertServiceException(() -> smsChannelService.deleteSmsChannel(id), SMS_CHANNEL_HAS_CHILDREN);
}
@Test
public void testGetSmsChannel() {
// mock 数据
SmsChannelDO dbSmsChannel = randomPojo(SmsChannelDO.class);
smsChannelMapper.insert(dbSmsChannel); // @Sql: 先插入出一条存在的数据
// 准备参数
Long id = dbSmsChannel.getId();
// 调用,并断言
assertPojoEquals(dbSmsChannel, smsChannelService.getSmsChannel(id));
}
@Test
public void testGetSmsChannelList() {
// mock 数据
SmsChannelDO dbSmsChannel01 = randomPojo(SmsChannelDO.class);
smsChannelMapper.insert(dbSmsChannel01);
SmsChannelDO dbSmsChannel02 = randomPojo(SmsChannelDO.class);
smsChannelMapper.insert(dbSmsChannel02);
// 准备参数
// 调用
List<SmsChannelDO> list = smsChannelService.getSmsChannelList();
// 断言
assertEquals(2, list.size());
assertPojoEquals(dbSmsChannel01, list.get(0));
assertPojoEquals(dbSmsChannel02, list.get(1));
}
@Test
public void testGetSmsChannelPage() {
// mock 数据
SmsChannelDO dbSmsChannel = randomPojo(SmsChannelDO.class, o -> { // 等会查询到
o.setSignature("晨丰科技");
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
o.setCreateTime(buildTime(2020, 12, 12));
});
smsChannelMapper.insert(dbSmsChannel);
// 测试 signature 不匹配
smsChannelMapper.insert(cloneIgnoreId(dbSmsChannel, o -> o.setSignature("源码")));
// 测试 status 不匹配
smsChannelMapper.insert(cloneIgnoreId(dbSmsChannel, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())));
// 测试 createTime 不匹配
smsChannelMapper.insert(cloneIgnoreId(dbSmsChannel, o -> o.setCreateTime(buildTime(2020, 11, 11))));
// 准备参数
SmsChannelPageReqVO reqVO = new SmsChannelPageReqVO();
reqVO.setSignature("晨丰");
reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
reqVO.setCreateTime(buildBetweenTime(2020, 12, 1, 2020, 12, 24));
// 调用
PageResult<SmsChannelDO> pageResult = smsChannelService.getSmsChannelPage(reqVO);
// 断言
assertEquals(1, pageResult.getTotal());
assertEquals(1, pageResult.getList().size());
assertPojoEquals(dbSmsChannel, pageResult.getList().get(0));
}
@Test
public void testGetSmsClient_id() {
// mock 数据
SmsChannelDO channel = randomPojo(SmsChannelDO.class);
smsChannelMapper.insert(channel);
// mock 参数
Long id = channel.getId();
// mock 方法
SmsClient mockClient = mock(SmsClient.class);
when(smsClientFactory.getSmsClient(eq(SmsChannelEnum.ALIYUN.getCode()))).thenReturn(mockClient);
// 调用
SmsClient client = smsChannelService.getSmsClient(id);
// 断言
assertSame(client, mockClient);
}
@Test
public void testGetSmsClient_code() {
// mock 数据
SmsChannelDO channel = randomPojo(SmsChannelDO.class);
smsChannelMapper.insert(channel);
// mock 参数
String code = channel.getCode();
// mock 方法
SmsClient mockClient = mock(SmsClient.class);
when(smsClientFactory.getSmsClient(eq(code))).thenReturn(mockClient);
// 调用
SmsClient client = smsChannelService.getSmsClient(code);
// 断言
assertSame(client, mockClient);
}
}
@@ -1,209 +1,353 @@
package com.cf.imes.module.system.service.sms;
import cn.hutool.core.map.MapUtil;
import com.cf.imes.framework.mybatis.core.enums.SqlConstants;
import com.cf.imes.framework.test.core.ut.BaseDbUnitTest;
import com.cf.imes.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.cf.imes.framework.security.test.WithMockLoginUser;
import com.cf.imes.framework.test.core.ut.BaseDbAndRedisUnitTest;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeValidateReqDTO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsCodeDO;
import com.cf.imes.module.system.dal.mysql.sms.SmsCodeMapper;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginSmsCheckReqVO;
import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO;
import com.cf.imes.module.system.enums.sms.SmsSceneEnum;
import com.cf.imes.module.system.framework.sms.config.SmsCodeConfiguration;
import com.cf.imes.module.system.framework.sms.config.SmsCodeProperties;
import com.baomidou.mybatisplus.annotation.DbType;
import com.cf.imes.module.system.service.user.AdminUserService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.mockito.Mockito;
import org.springframework.context.annotation.Import;
import jakarta.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import java.util.concurrent.TimeUnit;
import static cn.hutool.core.util.RandomUtil.randomEle;
import static com.cf.imes.framework.test.core.util.AssertUtils.assertPojoEquals;
import static com.cf.imes.framework.test.core.util.AssertUtils.assertServiceException;
import static com.cf.imes.framework.test.core.util.RandomUtils.randomPojo;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.*;
import static org.junit.jupiter.api.Assertions.*;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.AUTH_MOBILE_NOT_EXISTS;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.AUTH_MOBILE_NO_CHANGE;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.SMS_CODE_IS_VALID;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.SMS_CODE_NOT_CORRECT;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.SMS_CODE_NOT_FOUND;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.SMS_CODE_SEND_TOO_FAST;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.USER_MOBILE_EXISTS;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@Import(SmsCodeServiceImpl.class)
public class SmsCodeServiceImplTest extends BaseDbUnitTest {
@Import({SmsCodeServiceImpl.class, SmsCodeConfiguration.class})
public class SmsCodeServiceImplTest extends BaseDbAndRedisUnitTest {
@Resource
private SmsCodeServiceImpl smsCodeService;
@Resource
private SmsCodeMapper smsCodeMapper;
@MockitoBean
private RedisTemplate<String, Object> redisTemplate;
@MockBean
@MockitoBean
private ZSetOperations<String, Object> zSetOperations;
@MockitoBean
private ValueOperations<String, Object> valueOperations;
@MockitoBean
private AdminUserService userService;
@Resource
private SmsCodeProperties smsCodeProperties;
@MockBean
@MockitoBean
private SmsSendService smsSendService;
@BeforeEach
public void setUp() {
when(smsCodeProperties.getExpireTimes()).thenReturn(Duration.ofMinutes(5));
when(smsCodeProperties.getSendFrequency()).thenReturn(Duration.ofMinutes(1));
when(smsCodeProperties.getSendMaximumQuantityPerDay()).thenReturn(10);
when(smsCodeProperties.getBeginCode()).thenReturn(9999);
when(smsCodeProperties.getEndCode()).thenReturn(9999);
void setup() {
when(redisTemplate.opsForZSet()).thenReturn(zSetOperations);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
}
@Test
public void sendSmsCode_success() {
// 准备参数
SmsCodeSendReqDTO reqDTO = randomPojo(SmsCodeSendReqDTO.class, o -> {
o.setMobile("15601691300");
o.setScene(SmsSceneEnum.MEMBER_LOGIN.getScene());
});
// mock 方法
SqlConstants.init(DbType.MYSQL);
@WithMockLoginUser(userId = 1L)
void testSendSmsCode_userUpdateMobile_noChange() {
SmsCodeSendReqDTO reqDTO = new SmsCodeSendReqDTO();
reqDTO.setMobile("12345678901");
reqDTO.setScene(SmsSceneEnum.USER_UPDATE_MOBILE.getScene());
AdminUserDO user = new AdminUserDO();
user.setId(1L);
when(userService.getUserByUsernameAndOrganId(anyString(), any()))
.thenReturn(user);
assertServiceException(() -> smsCodeService.sendSmsCode(reqDTO), AUTH_MOBILE_NO_CHANGE);
}
@Test
void testSendSmsCode_notLogin() {
SmsCodeSendReqDTO reqDTO = new SmsCodeSendReqDTO();
reqDTO.setMobile("12345678901");
reqDTO.setScene(SmsSceneEnum.USER_UPDATE_MOBILE.getScene());
when(userService.getUserByUsernameAndOrganId(anyString(), any()))
.thenReturn(null);
assertServiceException(() -> smsCodeService.sendSmsCode(reqDTO), GlobalErrorCodeConstants.UNAUTHORIZED);
}
@Test
@WithMockLoginUser
void testSendSmsCode_userUpdateMobile_exists() {
SmsCodeSendReqDTO reqDTO = new SmsCodeSendReqDTO();
reqDTO.setMobile("12345678901");
reqDTO.setScene(SmsSceneEnum.USER_UPDATE_MOBILE.getScene());
AdminUserDO user = new AdminUserDO();
user.setId(2L);
when(userService.getUserByUsernameAndOrganId(anyString(), any()))
.thenReturn(user);
assertServiceException(() -> smsCodeService.sendSmsCode(reqDTO), USER_MOBILE_EXISTS);
}
@Test
@WithMockLoginUser
void testSendSmsCode_nonUpdateMobile_userNotExist() {
SmsCodeSendReqDTO reqDTO = new SmsCodeSendReqDTO();
reqDTO.setMobile("12345678901");
reqDTO.setScene(SmsSceneEnum.USER_RESET_PASSWORD.getScene());
when(userService.getUserByUsernameAndOrganId(anyString(), any()))
.thenReturn(null);
assertServiceException(() -> smsCodeService.sendSmsCode(reqDTO), AUTH_MOBILE_NOT_EXISTS);
}
@Test
@WithMockLoginUser
void testSendSmsCode_userUpdateMobile_success() {
SmsCodeSendReqDTO reqDTO = new SmsCodeSendReqDTO();
reqDTO.setMobile("12345678901");
reqDTO.setScene(SmsSceneEnum.USER_UPDATE_MOBILE.getScene());
when(userService.getUserByUsernameAndOrganId(anyString(), any()))
.thenReturn(null);
// 调用
smsCodeService.sendSmsCode(reqDTO);
// 断言 code 验证码
SmsCodeDO smsCodeDO = smsCodeMapper.selectOne(null);
assertPojoEquals(reqDTO, smsCodeDO);
assertEquals("9999", smsCodeDO.getCode());
assertEquals(1, smsCodeDO.getTodayIndex());
assertFalse(smsCodeDO.getUsed());
// 断言调用
verify(smsSendService).sendSingleSms(eq(reqDTO.getMobile()), isNull(), isNull(),
eq("user-sms-login"), eq(MapUtil.of("code", "9999")));
Mockito.verify(smsSendService).sendSingleSms(eq("12345678901"), isNull(), isNull(), anyString(), anyMap());
}
@Test
public void sendSmsCode_tooFast() {
// mock 数据
SmsCodeDO smsCodeDO = randomPojo(SmsCodeDO.class,
o -> o.setMobile("15601691300").setTodayIndex(1));
smsCodeMapper.insert(smsCodeDO);
// 准备参数
SmsCodeSendReqDTO reqDTO = randomPojo(SmsCodeSendReqDTO.class, o -> {
o.setMobile("15601691300");
o.setScene(SmsSceneEnum.MEMBER_LOGIN.getScene());
});
// mock 方法
SqlConstants.init(DbType.MYSQL);
@WithMockLoginUser
void testSendSmsCode_success() {
SmsCodeSendReqDTO reqDTO = new SmsCodeSendReqDTO();
reqDTO.setMobile("12345678901");
reqDTO.setScene(SmsSceneEnum.USER_RESET_PASSWORD.getScene());
reqDTO.setCreateIp("127.0.0.1");
// 调用,并断言异常
assertServiceException(() -> smsCodeService.sendSmsCode(reqDTO),
SMS_CODE_SEND_TOO_FAST);
AdminUserDO user = new AdminUserDO();
user.setId(1L);
when(userService.getUserByUsernameAndOrganId(anyString(), any()))
.thenReturn(user);
when(redisTemplate.hasKey(anyString())).thenReturn(false);
when(zSetOperations.zCard(anyString())).thenReturn(0L);
when(zSetOperations.add(anyString(), any(), anyDouble())).thenReturn(true);
when(redisTemplate.expire(anyString(), anyLong(), any(TimeUnit.class))).thenReturn(true);
when(smsSendService.sendSingleSms(
Mockito.eq("12345678901"),
Mockito.isNull(),
Mockito.isNull(),
Mockito.anyString(),
Mockito.anyMap()
)).thenReturn(123L); // 返回一个模拟的日志ID
smsCodeService.sendSmsCode(reqDTO);
Mockito.verify(smsSendService).sendSingleSms(eq("12345678901"), isNull(), isNull(), anyString(), anyMap());
}
@Test
public void sendSmsCode_exceedDay() {
// mock 数据
SmsCodeDO smsCodeDO = randomPojo(SmsCodeDO.class,
o -> o.setMobile("15601691300").setTodayIndex(10).setCreateTime(LocalDateTime.now()));
smsCodeMapper.insert(smsCodeDO);
// 准备参数
SmsCodeSendReqDTO reqDTO = randomPojo(SmsCodeSendReqDTO.class, o -> {
o.setMobile("15601691300");
o.setScene(SmsSceneEnum.MEMBER_LOGIN.getScene());
});
// mock 方法
SqlConstants.init(DbType.MYSQL);
when(smsCodeProperties.getSendFrequency()).thenReturn(Duration.ofMillis(0));
@WithMockLoginUser
void testSendSmsCode_moblibe_sendToFast() {
SmsCodeSendReqDTO reqDTO = new SmsCodeSendReqDTO();
reqDTO.setMobile("12345678901");
reqDTO.setScene(SmsSceneEnum.USER_RESET_PASSWORD.getScene());
reqDTO.setCreateIp("127.0.0.1");
AdminUserDO user = new AdminUserDO();
user.setId(1L);
when(userService.getUserByUsernameAndOrganId(anyString(), any()))
.thenReturn(user);
// 模拟redis校验发送次数为6次
when(zSetOperations.zCard(anyString())).thenReturn(6L);
assertServiceException(() -> smsCodeService.sendSmsCode(reqDTO), SMS_CODE_SEND_TOO_FAST);
// 调用,并断言异常
assertServiceException(() -> smsCodeService.sendSmsCode(reqDTO),
SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY);
}
@Test
public void testUseSmsCode_success() {
// 准备参数
SmsCodeUseReqDTO reqDTO = randomPojo(SmsCodeUseReqDTO.class, o -> {
o.setMobile("15601691300");
o.setScene(randomEle(SmsSceneEnum.values()).getScene());
});
// mock 数据
SqlConstants.init(DbType.MYSQL);
smsCodeMapper.insert(randomPojo(SmsCodeDO.class, o -> {
o.setMobile(reqDTO.getMobile()).setScene(reqDTO.getScene())
.setCode(reqDTO.getCode()).setUsed(false);
}));
@WithMockLoginUser
void testSendSmsCode_ip_sendToFast() {
SmsCodeSendReqDTO reqDTO = new SmsCodeSendReqDTO();
reqDTO.setMobile("12345678901");
reqDTO.setScene(SmsSceneEnum.USER_RESET_PASSWORD.getScene());
reqDTO.setCreateIp("127.0.0.1");
AdminUserDO user = new AdminUserDO();
user.setId(1L);
when(userService.getUserByUsernameAndOrganId(anyString(), any()))
.thenReturn(user);
// 模拟redis校验发送次数为6次
when(zSetOperations.zCard(anyString())).thenReturn(6L);
SmsCodeServiceImpl spyService = Mockito.spy(smsCodeService);
// 首次手机号校验发送次数返回true
Mockito.doReturn(true)
.doReturn(false)
.when(spyService)
.slidingWindowRateLimit(Mockito.anyString(), Mockito.anyInt(), Mockito.anyInt());
assertServiceException(() -> spyService.sendSmsCode(reqDTO), SMS_CODE_SEND_TOO_FAST);
// 调用
smsCodeService.useSmsCode(reqDTO);
// 断言
SmsCodeDO smsCodeDO = smsCodeMapper.selectOne(null);
assertTrue(smsCodeDO.getUsed());
assertNotNull(smsCodeDO.getUsedTime());
assertEquals(reqDTO.getUsedIp(), smsCodeDO.getUsedIp());
}
@Test
public void validateSmsCode_success() {
// 准备参数
SmsCodeValidateReqDTO reqDTO = randomPojo(SmsCodeValidateReqDTO.class, o -> {
o.setMobile("15601691300");
o.setScene(randomEle(SmsSceneEnum.values()).getScene());
});
// mock 数据
SqlConstants.init(DbType.MYSQL);
smsCodeMapper.insert(randomPojo(SmsCodeDO.class, o -> o.setMobile(reqDTO.getMobile())
.setScene(reqDTO.getScene()).setCode(reqDTO.getCode()).setUsed(false)));
void testSendSmsCode_codeExists() {
SmsCodeSendReqDTO reqDTO = new SmsCodeSendReqDTO();
reqDTO.setMobile("12345678901");
reqDTO.setScene(SmsSceneEnum.USER_RESET_PASSWORD.getScene());
// 调用
smsCodeService.validateSmsCode(reqDTO);
AdminUserDO user = new AdminUserDO();
user.setId(1L);
when(userService.getUserByUsernameAndOrganId(anyString(), any()))
.thenReturn(user);
// 模拟验证码已存在
when(redisTemplate.hasKey(anyString())).thenReturn(true);
assertServiceException(() -> smsCodeService.sendSmsCode(reqDTO), SMS_CODE_IS_VALID);
}
@Test
public void validateSmsCode_notFound() {
// 准备参数
SmsCodeValidateReqDTO reqDTO = randomPojo(SmsCodeValidateReqDTO.class, o -> {
o.setMobile("15601691300");
o.setScene(randomEle(SmsSceneEnum.values()).getScene());
});
// mock 数据
SqlConstants.init(DbType.MYSQL);
void testUseSmsCode_userNotExists() {
SmsCodeUseReqDTO req = SmsCodeUseReqDTO.builder()
.mobile("13800000000")
.build();
// 调用,并断言异常
assertServiceException(() -> smsCodeService.validateSmsCode(reqDTO),
SMS_CODE_NOT_FOUND);
when(userService.getUserUniqueByUserName(req.getMobile())).thenReturn(null);
assertServiceException(() -> smsCodeService.useSmsCode(req), AUTH_MOBILE_NOT_EXISTS);
}
@Test
public void validateSmsCode_expired() {
// 准备参数
SmsCodeValidateReqDTO reqDTO = randomPojo(SmsCodeValidateReqDTO.class, o -> {
o.setMobile("15601691300");
o.setScene(randomEle(SmsSceneEnum.values()).getScene());
});
// mock 数据
SqlConstants.init(DbType.MYSQL);
smsCodeMapper.insert(randomPojo(SmsCodeDO.class, o -> o.setMobile(reqDTO.getMobile())
.setScene(reqDTO.getScene()).setCode(reqDTO.getCode()).setUsed(false)
.setCreateTime(LocalDateTime.now().minusMinutes(6))));
void testUseSmsCode_codeNotFound() {
SmsCodeUseReqDTO req = SmsCodeUseReqDTO.builder()
.mobile("13800000000")
.scene(SmsSceneEnum.USER_RESET_PASSWORD.getScene())
.code("123456")
.build();
// 调用,并断言异常
assertServiceException(() -> smsCodeService.validateSmsCode(reqDTO),
SMS_CODE_EXPIRED);
when(userService.getUserUniqueByUserName(req.getMobile())).thenReturn(new AdminUserDO());
when(valueOperations.get(Mockito.anyString())).thenReturn(null);
assertServiceException(() -> smsCodeService.useSmsCode(req), SMS_CODE_NOT_FOUND);
}
@Test
public void validateSmsCode_used() {
// 准备参数
SmsCodeValidateReqDTO reqDTO = randomPojo(SmsCodeValidateReqDTO.class, o -> {
o.setMobile("15601691300");
o.setScene(randomEle(SmsSceneEnum.values()).getScene());
});
// mock 数据
SqlConstants.init(DbType.MYSQL);
smsCodeMapper.insert(randomPojo(SmsCodeDO.class, o -> o.setMobile(reqDTO.getMobile())
.setScene(reqDTO.getScene()).setCode(reqDTO.getCode()).setUsed(true)
.setCreateTime(LocalDateTime.now())));
void testUseSmsCode_codeIncorrect_shouldThrow() {
SmsCodeUseReqDTO req = SmsCodeUseReqDTO.builder()
.mobile("13800000000")
.scene(SmsSceneEnum.USER_RESET_PASSWORD.getScene())
.code("654321")
.build();
// 调用,并断言异常
assertServiceException(() -> smsCodeService.validateSmsCode(reqDTO),
SMS_CODE_USED);
when(userService.getUserUniqueByUserName(req.getMobile())).thenReturn(new AdminUserDO());
when(valueOperations.get(Mockito.anyString())).thenReturn("123456");
assertServiceException(() -> smsCodeService.useSmsCode(req), SMS_CODE_NOT_CORRECT);
}
@Test
void testUseSmsCode_success() {
SmsCodeUseReqDTO req = SmsCodeUseReqDTO.builder()
.mobile("13800000000")
.scene(SmsSceneEnum.USER_UPDATE_MOBILE.getScene())
.code("123456")
.build();
when(userService.getUserUniqueByUserName(req.getMobile())).thenReturn(null);
when(valueOperations.get(Mockito.anyString())).thenReturn("123456");
smsCodeService.useSmsCode(req);
Mockito.verify(redisTemplate).delete(Mockito.anyString());
}
@Test
void testValidateSmsCode_userNotExists_shouldThrowAuthMobileNotExists() {
AuthLoginSmsCheckReqVO reqVO = new AuthLoginSmsCheckReqVO();
reqVO.setMobile("13800000000");
when(userService.getUserUniqueByUserName("13800000000")).thenReturn(null);
assertServiceException(() -> smsCodeService.validateSmsCode(reqVO), AUTH_MOBILE_NOT_EXISTS);
}
@Test
void testValidateSmsCode_smsCodeNotFound_shouldThrowSmsCodeNotFound() {
AuthLoginSmsCheckReqVO reqVO = new AuthLoginSmsCheckReqVO();
reqVO.setMobile("13800000000");
when(userService.getUserUniqueByUserName("13800000000")).thenReturn(new AdminUserDO());
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(valueOperations.get(anyString())).thenReturn(null);
assertServiceException(() -> smsCodeService.validateSmsCode(reqVO), SMS_CODE_NOT_FOUND);
}
@Test
void testValidateSmsCode_smsCodeNotCorrect_shouldThrowSmsCodeNotCorrect() {
AuthLoginSmsCheckReqVO reqVO = new AuthLoginSmsCheckReqVO();
reqVO.setMobile("13800000000");
reqVO.setSmsCaptchaVerification("123456");
when(userService.getUserUniqueByUserName("13800000000")).thenReturn(new AdminUserDO());
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(valueOperations.get(anyString())).thenReturn("999999");
assertServiceException(() -> smsCodeService.validateSmsCode(reqVO), SMS_CODE_NOT_CORRECT);
}
@Test
void testValidateSmsCode_smsCodeCorrect_shouldPass() {
AuthLoginSmsCheckReqVO reqVO = new AuthLoginSmsCheckReqVO();
reqVO.setMobile("13800000000");
reqVO.setSmsCaptchaVerification("123456");
when(userService.getUserUniqueByUserName("13800000000")).thenReturn(new AdminUserDO());
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(valueOperations.get(anyString())).thenReturn("123456");
assertDoesNotThrow(() -> smsCodeService.validateSmsCode(reqVO));
}
}
@@ -4,7 +4,7 @@ import cn.hutool.core.map.MapUtil;
import com.cf.imes.framework.common.enums.UserTypeEnum;
import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.framework.common.util.collection.ArrayUtils;
import com.cf.imes.framework.test.core.ut.BaseDbUnitTest;
import com.cf.imes.framework.test.core.ut.BaseDbAndRedisUnitTest;
import com.cf.imes.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsLogDO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsTemplateDO;
@@ -16,6 +16,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import jakarta.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.function.Consumer;
@@ -31,7 +32,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@Import(SmsLogServiceImpl.class)
public class SmsLogServiceImplTest extends BaseDbUnitTest {
public class SmsLogServiceImplTest extends BaseDbAndRedisUnitTest {
@Resource
private SmsLogServiceImpl smsLogService;
@@ -40,7 +41,7 @@ public class SmsLogServiceImplTest extends BaseDbUnitTest {
private SmsLogMapper smsLogMapper;
@Test
public void testGetSmsLogPage() {
void testGetSmsLogPage() {
// mock 数据
SmsLogDO dbSmsLog = randomSmsLogDO(o -> { // 等会查询到
o.setChannelId(1L);
@@ -85,7 +86,7 @@ public class SmsLogServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testCreateSmsLog() {
void testCreateSmsLog() {
// 准备参数
String mobile = randomString();
Long userId = randomLongId();
@@ -102,30 +103,26 @@ public class SmsLogServiceImplTest extends BaseDbUnitTest {
templateDO, templateContent, templateParams);
// 断言
SmsLogDO logDO = smsLogMapper.selectById(logId);
assertEquals(isSend ? SmsSendStatusEnum.INIT.getStatus() : SmsSendStatusEnum.IGNORE.getStatus(),
logDO.getSendStatus());
assertEquals(SmsSendStatusEnum.INIT.getStatus(), logDO.getSendStatus());
assertEquals(mobile, logDO.getMobile());
assertEquals(userType, logDO.getUserType());
assertEquals(userId, logDO.getUserId());
assertEquals(templateDO.getId(), logDO.getTemplateId());
assertEquals(templateDO.getCode(), logDO.getTemplateCode());
assertEquals(templateDO.getType(), logDO.getTemplateType());
assertEquals(templateDO.getChannelId(), logDO.getChannelId());
assertEquals(templateDO.getChannelCode(), logDO.getChannelCode());
assertEquals(templateContent, logDO.getTemplateContent());
assertEquals(templateParams, logDO.getTemplateParams());
assertEquals(SmsReceiveStatusEnum.INIT.getStatus(), logDO.getReceiveStatus());
}
@Test
public void testUpdateSmsSendResult() {
void testUpdateSmsSendResult() {
// mock 数据
SmsLogDO dbSmsLog = randomSmsLogDO(
o -> o.setSendStatus(SmsSendStatusEnum.IGNORE.getStatus()));
smsLogMapper.insert(dbSmsLog);
// 准备参数
Long id = dbSmsLog.getId();
Boolean success = randomBoolean();
String apiSendCode = randomString();
String apiSendMsg = randomString();
String apiRequestId = randomString();
@@ -133,41 +130,51 @@ public class SmsLogServiceImplTest extends BaseDbUnitTest {
String channelCode = randomString();
// 调用
smsLogService.updateSmsSendResult(id, success,
smsLogService.updateSmsSendResult(id, true,
apiSendCode, apiSendMsg, apiRequestId, apiSerialNo, channelCode);
// 断言
dbSmsLog = smsLogMapper.selectById(id);
assertEquals(success ? SmsSendStatusEnum.SUCCESS.getStatus() : SmsSendStatusEnum.FAILURE.getStatus(),
dbSmsLog.getSendStatus());
assertEquals(SmsSendStatusEnum.SUCCESS.getStatus(), dbSmsLog.getSendStatus());
assertNotNull(dbSmsLog.getSendTime());
assertEquals(apiSendCode, dbSmsLog.getApiSendCode());
assertEquals(apiSendMsg, dbSmsLog.getApiSendMsg());
assertEquals(apiRequestId, dbSmsLog.getApiRequestId());
assertEquals(apiSerialNo, dbSmsLog.getApiSerialNo());
// 调用
smsLogService.updateSmsSendResult(id, false,
apiSendCode, apiSendMsg, apiRequestId, apiSerialNo, channelCode);
// 断言
dbSmsLog = smsLogMapper.selectById(id);
assertEquals(SmsSendStatusEnum.FAILURE.getStatus(), dbSmsLog.getSendStatus());
}
@Test
public void testUpdateSmsReceiveResult() {
void testUpdateSmsReceiveResult() {
// mock 数据
SmsLogDO dbSmsLog = randomSmsLogDO(
o -> o.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus()));
smsLogMapper.insert(dbSmsLog);
// 准备参数
Long id = dbSmsLog.getId();
Boolean success = randomBoolean();
LocalDateTime receiveTime = randomLocalDateTime();
String apiReceiveCode = randomString();
String apiReceiveMsg = randomString();
// 调用
smsLogService.updateSmsReceiveResult(id, success, receiveTime, apiReceiveCode, apiReceiveMsg);
smsLogService.updateSmsReceiveResult(id, true, receiveTime, apiReceiveCode, apiReceiveMsg);
// 断言
dbSmsLog = smsLogMapper.selectById(id);
assertEquals(success ? SmsReceiveStatusEnum.SUCCESS.getStatus()
: SmsReceiveStatusEnum.FAILURE.getStatus(), dbSmsLog.getReceiveStatus());
assertEquals(SmsReceiveStatusEnum.SUCCESS.getStatus(), dbSmsLog.getReceiveStatus());
assertEquals(receiveTime, dbSmsLog.getReceiveTime());
assertEquals(apiReceiveCode, dbSmsLog.getApiReceiveCode());
assertEquals(apiReceiveMsg, dbSmsLog.getApiReceiveMsg());
// 调用
smsLogService.updateSmsReceiveResult(id, false, receiveTime, apiReceiveCode, apiReceiveMsg);
// 断言
dbSmsLog = smsLogMapper.selectById(id);
assertEquals(SmsReceiveStatusEnum.FAILURE.getStatus(), dbSmsLog.getReceiveStatus());
}
// ========== 随机对象 ==========
@@ -4,23 +4,26 @@ import cn.hutool.core.map.MapUtil;
import com.cf.imes.framework.common.core.KeyValue;
import com.cf.imes.framework.common.enums.CommonStatusEnum;
import com.cf.imes.framework.common.enums.UserTypeEnum;
import com.cf.imes.framework.test.core.ut.BaseRedisUnitTest;
import com.cf.imes.module.system.framework.sms.config.SmsCodeConfiguration;
import com.cf.imes.module.system.framework.sms.config.SmsCodeProperties;
import com.cf.imes.module.system.framework.sms.core.client.SmsClient;
import com.cf.imes.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
import com.cf.imes.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
import com.cf.imes.framework.test.core.ut.BaseMockitoUnitTest;
import com.cf.imes.module.system.dal.dataobject.sms.SmsChannelDO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsTemplateDO;
import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO;
import com.cf.imes.module.system.mq.message.sms.SmsSendMessage;
import com.cf.imes.module.system.mq.producer.sms.SmsProducer;
import com.cf.imes.module.system.service.member.MemberService;
import com.cf.imes.module.system.service.user.AdminUserService;
import jakarta.annotation.Resource;
import org.assertj.core.util.Lists;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -29,110 +32,46 @@ import static cn.hutool.core.util.RandomUtil.randomEle;
import static com.cf.imes.framework.test.core.util.AssertUtils.assertServiceException;
import static com.cf.imes.framework.test.core.util.RandomUtils.*;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.*;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
@Import({SmsSendServiceImpl.class, SmsCodeConfiguration.class})
public class SmsSendServiceImplTest extends BaseRedisUnitTest {
@InjectMocks
@Resource
private SmsSendServiceImpl smsSendService;
@Mock
private AdminUserService adminUserService;
@Mock
private MemberService memberService;
@Mock
@MockitoBean
private SmsChannelService smsChannelService;
@Mock
@MockitoBean
private SmsTemplateService smsTemplateService;
@Mock
@MockitoBean
private SmsLogService smsLogService;
@Mock
@MockitoBean
private SmsProducer smsProducer;
@Test
public void testSendSingleSmsToAdmin() {
// 准备参数
Long userId = randomLongId();
String templateCode = randomString();
Map<String, Object> templateParams = MapUtil.<String, Object>builder().put("code", "1234")
.put("op", "login").build();
// mock adminUserService 的方法
AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setMobile("15601691300"));
when(adminUserService.getUser(eq(userId))).thenReturn(user);
@Resource
private SmsCodeProperties smsCodeProperties;
// mock SmsTemplateService 的方法
SmsTemplateDO template = randomPojo(SmsTemplateDO.class, o -> {
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
o.setContent("验证码为{code}, 操作为{op}");
o.setParams(Lists.newArrayList("code", "op"));
});
when(smsTemplateService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template);
String content = randomString();
when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams)))
.thenReturn(content);
// mock SmsChannelService 的方法
SmsChannelDO smsChannel = randomPojo(SmsChannelDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()));
when(smsChannelService.getSmsChannel(eq(template.getChannelId()))).thenReturn(smsChannel);
// mock SmsLogService 的方法
Long smsLogId = randomLongId();
when(smsLogService.createSmsLog(eq(user.getMobile()), eq(userId), eq(UserTypeEnum.ADMIN.getValue()), eq(Boolean.TRUE), eq(template),
eq(content), eq(templateParams))).thenReturn(smsLogId);
@MockitoBean
private RedisTemplate<String, Object> redisTemplate;
// 调用
Long resultSmsLogId = smsSendService.sendSingleSmsToAdmin(null, userId, templateCode, templateParams);
// 断言
assertEquals(smsLogId, resultSmsLogId);
// 断言调用
verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(user.getMobile()), eq(template),
eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login"))));
}
@MockitoBean
private ValueOperations<String, Object> valueOperations;
@Test
public void testSendSingleSmsToUser() {
// 准备参数
Long userId = randomLongId();
String templateCode = randomString();
Map<String, Object> templateParams = MapUtil.<String, Object>builder().put("code", "1234")
.put("op", "login").build();
// mock memberService 的方法
String mobile = "15601691300";
when(memberService.getMemberUserMobile(eq(userId))).thenReturn(mobile);
// mock SmsTemplateService 的方法
SmsTemplateDO template = randomPojo(SmsTemplateDO.class, o -> {
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
o.setContent("验证码为{code}, 操作为{op}");
o.setParams(Lists.newArrayList("code", "op"));
});
when(smsTemplateService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template);
String content = randomString();
when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams)))
.thenReturn(content);
// mock SmsChannelService 的方法
SmsChannelDO smsChannel = randomPojo(SmsChannelDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()));
when(smsChannelService.getSmsChannel(eq(template.getChannelId()))).thenReturn(smsChannel);
// mock SmsLogService 的方法
Long smsLogId = randomLongId();
when(smsLogService.createSmsLog(eq(mobile), eq(userId), eq(UserTypeEnum.MEMBER.getValue()), eq(Boolean.TRUE), eq(template),
eq(content), eq(templateParams))).thenReturn(smsLogId);
// 调用
Long resultSmsLogId = smsSendService.sendSingleSmsToMember(null, userId, templateCode, templateParams);
// 断言
assertEquals(smsLogId, resultSmsLogId);
// 断言调用
verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile), eq(template),
eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login"))));
@BeforeEach
void setUp() {
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
}
/**
* 发送成功,当短信模板开启时
*/
@Test
public void testSendSingleSms_successWhenSmsTemplateEnable() {
void testSendSingleSms_successWhenSmsTemplateEnable() {
// 准备参数
String mobile = randomString();
Long userId = randomLongId();
@@ -150,9 +89,6 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
String content = randomString();
when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams)))
.thenReturn(content);
// mock SmsChannelService 的方法
SmsChannelDO smsChannel = randomPojo(SmsChannelDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()));
when(smsChannelService.getSmsChannel(eq(template.getChannelId()))).thenReturn(smsChannel);
// mock SmsLogService 的方法
Long smsLogId = randomLongId();
when(smsLogService.createSmsLog(eq(mobile), eq(userId), eq(userType), eq(Boolean.TRUE), eq(template),
@@ -171,7 +107,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
* 发送成功,当短信模板关闭时
*/
@Test
public void testSendSingleSms_successWhenSmsTemplateDisable() {
void testSendSingleSms_successWhenSmsTemplateDisable() {
// 准备参数
String mobile = randomString();
Long userId = randomLongId();
@@ -189,9 +125,6 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
String content = randomString();
when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams)))
.thenReturn(content);
// mock SmsChannelService 的方法
SmsChannelDO smsChannel = randomPojo(SmsChannelDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()));
when(smsChannelService.getSmsChannel(eq(template.getChannelId()))).thenReturn(smsChannel);
// mock SmsLogService 的方法
Long smsLogId = randomLongId();
when(smsLogService.createSmsLog(eq(mobile), eq(userId), eq(userType), eq(Boolean.FALSE), eq(template),
@@ -206,7 +139,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
}
@Test
public void testCheckSmsTemplateValid_notExists() {
void testCheckSmsTemplateValid_notExists() {
// 准备参数
String templateCode = randomString();
// mock 方法
@@ -217,7 +150,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
}
@Test
public void testBuildTemplateParams_paramMiss() {
void testBuildTemplateParams_paramMiss() {
// 准备参数
SmsTemplateDO template = randomPojo(SmsTemplateDO.class,
o -> o.setParams(Lists.newArrayList("code")));
@@ -230,7 +163,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
}
@Test
public void testCheckMobile_notExists() {
void testCheckMobile_notExists() {
// 准备参数
// mock 方法
@@ -240,7 +173,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
}
@Test
public void testSendBatchNotify() {
void testSendBatchNotify() {
// 准备参数
// mock 方法
@@ -255,38 +188,91 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
@Test
@SuppressWarnings("unchecked")
public void testDoSendSms() throws Throwable {
void testDoSendSms() throws Throwable {
// 准备参数
SmsSendMessage message = randomPojo(SmsSendMessage.class);
message.setMobile("15601691300");
message.setTemplateParams(List.of(new KeyValue<>("code", "123456")));
// mock SmsClientFactory 的方法
SmsClient smsClient = spy(SmsClient.class);
when(smsChannelService.getSmsClient(eq(message.getChannelId()))).thenReturn(smsClient);
SmsClient smsClient = mock(SmsClient.class);
when(smsChannelService.getSmsClient()).thenReturn(smsClient);
SmsSendRespDTO sendResp = randomPojo(SmsSendRespDTO.class, dto -> dto.setSuccess(true));
// mock SmsClient 的方法
SmsSendRespDTO sendResult = randomPojo(SmsSendRespDTO.class);
when(smsClient.sendSms(eq(message.getLogId()), eq(message.getMobile()), eq(message.getApiTemplateId()),
eq(message.getTemplateParams()))).thenReturn(sendResult);
eq(message.getTemplateParams()))).thenReturn(sendResp);
// 调用
smsSendService.doSendSms(message);
// 断言
verify(smsLogService).updateSmsSendResult(eq(message.getLogId()),
eq(sendResult.getSuccess()), eq(sendResult.getApiCode()),
eq(sendResult.getApiMsg()), eq(sendResult.getApiRequestId()), eq(sendResult.getSerialNo()), eq(sendResult.getChannelCode()));
verify(valueOperations).set(
contains("sms_captcha_verification"),
eq("123456"),
eq(Duration.ofMinutes(5))
);
verify(smsLogService).updateSmsSendResult(
eq(message.getLogId()), eq(true),
eq(sendResp.getApiCode()), eq(sendResp.getApiMsg()),
eq(sendResp.getApiRequestId()), eq(sendResp.getSerialNo()), eq(sendResp.getChannelCode())
);
}
@Test
public void testReceiveSmsStatus() throws Throwable {
void testDoSendSms_fail() throws Throwable {
SmsSendMessage message = randomPojo(SmsSendMessage.class);
message.setTemplateParams(List.of(new KeyValue<>("code", "999999")));
// mock SmsClientFactory 的方法
SmsClient smsClient = spy(SmsClient.class);
when(smsChannelService.getSmsClient()).thenReturn(smsClient);
SmsSendRespDTO sendResp = randomPojo(SmsSendRespDTO.class, dto -> dto.setSuccess(false));
when(smsClient.sendSms(anyLong(), anyString(), anyString(), anyList())).thenReturn(sendResp);
smsSendService.doSendSms(message);
// 不应写 redis
verify(redisTemplate, never()).opsForValue();
verify(smsLogService).updateSmsSendResult(
eq(message.getLogId()), eq(false),
eq(sendResp.getApiCode()), eq(sendResp.getApiMsg()),
eq(sendResp.getApiRequestId()), eq(sendResp.getSerialNo()), eq(sendResp.getChannelCode())
);
}
@Test
void testDoSendSms_throwExceptionDuringSend() throws Throwable {
SmsSendMessage message = randomPojo(SmsSendMessage.class);
// mock SmsClientFactory 的方法
SmsClient smsClient = spy(SmsClient.class);
when(smsChannelService.getSmsClient()).thenReturn(smsClient);
when(smsClient.sendSms(anyLong(), anyString(), anyString(), anyList()))
.thenThrow(new RuntimeException("发送异常"));
smsSendService.doSendSms(message);
verify(smsLogService).updateSmsSendResult(
eq(message.getLogId()), eq(false),
eq("EXCEPTION"), contains("发送异常"),
isNull(), isNull(), isNull()
);
}
@Test
void testReceiveSmsStatus() throws Throwable {
// 准备参数
String channelCode = randomString();
String text = randomString();
// mock SmsClientFactory 的方法
SmsClient smsClient = spy(SmsClient.class);
when(smsChannelService.getSmsClient(eq(channelCode))).thenReturn(smsClient);
when(smsChannelService.getSmsClient()).thenReturn(smsClient);
// mock SmsClient 的方法
List<SmsReceiveRespDTO> receiveResults = randomPojoList(SmsReceiveRespDTO.class);
when(smsClient.parseSmsReceiveStatus(text)).thenReturn(receiveResults);
// 调用
assertThrows(Throwable.class, () -> smsSendService.receiveSmsStatus(channelCode, text));
assertDoesNotThrow(() -> smsSendService.receiveSmsStatus(channelCode, text));
// 断言
receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(eq(result.getLogId()), eq(result.getSuccess()),
eq(result.getReceiveTime()), eq(result.getErrorCode()), eq(result.getErrorCode())));
@@ -2,25 +2,28 @@ package com.cf.imes.module.system.service.sms;
import cn.hutool.core.map.MapUtil;
import com.cf.imes.framework.common.enums.CommonStatusEnum;
import com.cf.imes.framework.common.exception.ServiceException;
import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.framework.common.util.collection.ArrayUtils;
import com.cf.imes.framework.common.util.object.ObjectUtils;
import com.cf.imes.framework.test.core.ut.BaseDbAndRedisUnitTest;
import com.cf.imes.module.system.controller.admin.sms.vo.template.SmsTemplateRespVO;
import com.cf.imes.module.system.enums.ErrorCodeConstants;
import com.cf.imes.module.system.framework.sms.core.client.SmsClient;
import com.cf.imes.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
import com.cf.imes.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import com.cf.imes.framework.test.core.ut.BaseDbUnitTest;
import com.cf.imes.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO;
import com.cf.imes.module.system.controller.admin.sms.vo.template.SmsTemplateSaveReqVO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsChannelDO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsTemplateDO;
import com.cf.imes.module.system.dal.mysql.sms.SmsTemplateMapper;
import com.cf.imes.module.system.enums.sms.SmsTemplateTypeEnum;
import com.google.common.collect.Lists;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import jakarta.annotation.Resource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
@@ -34,10 +37,12 @@ import static com.cf.imes.framework.test.core.util.RandomUtils.*;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;
@Import(SmsTemplateServiceImpl.class)
public class SmsTemplateServiceImplTest extends BaseDbUnitTest {
public class SmsTemplateServiceImplTest extends BaseDbAndRedisUnitTest {
@Resource
private SmsTemplateServiceImpl smsTemplateService;
@@ -45,13 +50,14 @@ public class SmsTemplateServiceImplTest extends BaseDbUnitTest {
@Resource
private SmsTemplateMapper smsTemplateMapper;
@MockBean
@MockitoBean
private SmsChannelService smsChannelService;
@MockBean
@MockitoBean
private SmsClient smsClient;
@Test
public void testFormatSmsTemplateContent() {
void testFormatSmsTemplateContent() {
// 准备参数
String content = "正在进行登录操作{operation},您的验证码是{code}";
Map<String, Object> params = MapUtil.<String, Object>builder("operation", "登录")
@@ -64,7 +70,7 @@ public class SmsTemplateServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testParseTemplateContentParams() {
void testParseTemplateContentParams() {
// 准备参数
String content = "正在进行登录操作{operation},您的验证码是{code}";
// mock 方法
@@ -77,7 +83,7 @@ public class SmsTemplateServiceImplTest extends BaseDbUnitTest {
@Test
@SuppressWarnings("unchecked")
public void testCreateSmsTemplate_success() throws Throwable {
void testCreateSmsTemplate_success() throws Throwable {
// 准备参数
SmsTemplateSaveReqVO reqVO = randomPojo(SmsTemplateSaveReqVO.class, o -> {
o.setContent("正在进行登录操作{operation},您的验证码是{code}");
@@ -101,7 +107,7 @@ public class SmsTemplateServiceImplTest extends BaseDbUnitTest {
@Test
@SuppressWarnings("unchecked")
public void testUpdateSmsTemplate_success() throws Throwable {
void testUpdateSmsTemplate_success() throws Throwable {
// mock 数据
SmsTemplateDO dbSmsTemplate = randomSmsTemplateDO();
smsTemplateMapper.insert(dbSmsTemplate);// @Sql: 先插入出一条存在的数据
@@ -126,7 +132,7 @@ public class SmsTemplateServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testUpdateSmsTemplate_notExists() {
void testUpdateSmsTemplate_notExists() {
// 准备参数
SmsTemplateSaveReqVO reqVO = randomPojo(SmsTemplateSaveReqVO.class);
@@ -135,7 +141,7 @@ public class SmsTemplateServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testDeleteSmsTemplate_success() {
void testDeleteSmsTemplate_success() {
// mock 数据
SmsTemplateDO dbSmsTemplate = randomSmsTemplateDO();
smsTemplateMapper.insert(dbSmsTemplate);// @Sql: 先插入出一条存在的数据
@@ -149,7 +155,7 @@ public class SmsTemplateServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testDeleteSmsTemplate_notExists() {
void testDeleteSmsTemplate_notExists() {
// 准备参数
Long id = randomLongId();
@@ -158,7 +164,7 @@ public class SmsTemplateServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testGetSmsTemplate() {
void testGetSmsTemplate() {
// mock 数据
SmsTemplateDO dbSmsTemplate = randomSmsTemplateDO();
smsTemplateMapper.insert(dbSmsTemplate);// @Sql: 先插入出一条存在的数据
@@ -172,7 +178,7 @@ public class SmsTemplateServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testGetSmsTemplateByCodeFromCache() {
void testGetSmsTemplateByCodeFromCache() {
// mock 数据
SmsTemplateDO dbSmsTemplate = randomSmsTemplateDO();
smsTemplateMapper.insert(dbSmsTemplate);// @Sql: 先插入出一条存在的数据
@@ -186,7 +192,7 @@ public class SmsTemplateServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testGetSmsTemplatePage() {
void testGetSmsTemplatePage() {
// mock 数据
SmsTemplateDO dbSmsTemplate = randomPojo(SmsTemplateDO.class, o -> { // 等会查询到
o.setType(SmsTemplateTypeEnum.PROMOTION.getType());
@@ -194,7 +200,6 @@ public class SmsTemplateServiceImplTest extends BaseDbUnitTest {
o.setCode("tudou");
o.setContent("晨丰科技");
o.setApiTemplateId("yunai");
o.setChannelId(1L);
o.setCreateTime(buildTime(2021, 11, 11));
});
smsTemplateMapper.insert(dbSmsTemplate);
@@ -204,12 +209,8 @@ public class SmsTemplateServiceImplTest extends BaseDbUnitTest {
smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())));
// 测试 code 不匹配
smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setCode("yuanma")));
// 测试 content 不匹配
smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setContent("源码")));
// 测试 apiTemplateId 不匹配
smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setApiTemplateId("nai")));
// 测试 channelId 不匹配
smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setChannelId(2L)));
// 测试 createTime 不匹配
smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setCreateTime(buildTime(2021, 12, 12))));
// 准备参数
@@ -229,7 +230,7 @@ public class SmsTemplateServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testGetSmsTemplateCountByChannelId() {
void testGetSmsTemplateCountByChannelId() {
// mock 数据
SmsTemplateDO dbSmsTemplate = randomPojo(SmsTemplateDO.class, o -> o.setChannelId(1L));
smsTemplateMapper.insert(dbSmsTemplate);
@@ -245,56 +246,13 @@ public class SmsTemplateServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testValidateSmsChannel_success() {
// 准备参数
Long channelId = randomLongId();
// mock 方法
SmsChannelDO channelDO = randomPojo(SmsChannelDO.class, o -> {
o.setId(channelId);
o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 开启,创建必须处于这个状态
});
when(smsChannelService.getSmsChannel(eq(channelId))).thenReturn(channelDO);
// 调用
SmsChannelDO returnChannelDO = smsTemplateService.validateSmsChannel(channelId);
// 断言
assertPojoEquals(returnChannelDO, channelDO);
}
@Test
public void testValidateSmsChannel_notExists() {
// 准备参数
Long channelId = randomLongId();
// 调用,校验异常
assertServiceException(() -> smsTemplateService.validateSmsChannel(channelId),
SMS_CHANNEL_NOT_EXISTS);
}
@Test
public void testValidateSmsChannel_disable() {
// 准备参数
Long channelId = randomLongId();
// mock 方法
SmsChannelDO channelDO = randomPojo(SmsChannelDO.class, o -> {
o.setId(channelId);
o.setStatus(CommonStatusEnum.DISABLE.getStatus()); // 保证 status 禁用,触发失败
});
when(smsChannelService.getSmsChannel(eq(channelId))).thenReturn(channelDO);
// 调用,校验异常
assertServiceException(() -> smsTemplateService.validateSmsChannel(channelId),
SMS_CHANNEL_DISABLE);
}
@Test
public void testValidateDictDataValueUnique_success() {
void testValidateDictDataValueUnique_success() {
// 调用,成功
smsTemplateService.validateSmsTemplateCodeDuplicate(randomLongId(), randomString());
}
@Test
public void testValidateSmsTemplateCodeDuplicate_valueDuplicateForCreate() {
void testValidateSmsTemplateCodeDuplicate_valueDuplicateForCreate() {
// 准备参数
String code = randomString();
// mock 数据
@@ -306,7 +264,7 @@ public class SmsTemplateServiceImplTest extends BaseDbUnitTest {
}
@Test
public void testValidateDictDataValueUnique_valueDuplicateForUpdate() {
void testValidateDictDataValueUnique_valueDuplicateForUpdate() {
// 准备参数
Long id = randomLongId();
String code = randomString();
@@ -329,4 +287,50 @@ public class SmsTemplateServiceImplTest extends BaseDbUnitTest {
return randomPojo(SmsTemplateDO.class, ArrayUtils.append(consumer, consumers));
}
@Test
void testValidateApiTemplate_allBranches() throws Throwable {
String apiTemplateId = "tpl-123";
// 1. smsClient 为 null
doReturn(null).when(smsChannelService).getSmsClient();
ServiceException ex1 = assertThrows(ServiceException.class,
() -> smsTemplateService.validateApiTemplate(apiTemplateId));
assertEquals(ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS.getCode(), ex1.getCode());
// 2. smsClient 不为 null,但 getSmsTemplate 抛异常
doReturn(smsClient).when(smsChannelService).getSmsClient();
doThrow(new RuntimeException("api error")).when(smsClient).getSmsTemplate(apiTemplateId);
ServiceException ex2 = assertThrows(ServiceException.class,
() -> smsTemplateService.validateApiTemplate(apiTemplateId));
assertEquals(ErrorCodeConstants.SMS_TEMPLATE_API_ERROR.getCode(), ex2.getCode());
// 3. 返回 null 模板
doReturn(null).when(smsClient).getSmsTemplate(apiTemplateId);
ServiceException ex3 = assertThrows(ServiceException.class,
() -> smsTemplateService.validateApiTemplate(apiTemplateId));
assertEquals(ErrorCodeConstants.SMS_TEMPLATE_API_NOT_FOUND.getCode(), ex3.getCode());
// 4. 审核中
SmsTemplateRespDTO checkingTpl = new SmsTemplateRespDTO();
checkingTpl.setAuditStatus(SmsTemplateAuditStatusEnum.CHECKING.getStatus());
doReturn(checkingTpl).when(smsClient).getSmsTemplate(apiTemplateId);
ServiceException ex4 = assertThrows(ServiceException.class,
() -> smsTemplateService.validateApiTemplate(apiTemplateId));
assertEquals(ErrorCodeConstants.SMS_TEMPLATE_API_AUDIT_CHECKING.getCode(), ex4.getCode());
// 5. 审核失败
SmsTemplateRespDTO failTpl = new SmsTemplateRespDTO();
failTpl.setAuditStatus(SmsTemplateAuditStatusEnum.FAIL.getStatus());
failTpl.setAuditReason("reason");
doReturn(failTpl).when(smsClient).getSmsTemplate(apiTemplateId);
ServiceException ex5 = assertThrows(ServiceException.class,
() -> smsTemplateService.validateApiTemplate(apiTemplateId));
assertEquals(ErrorCodeConstants.SMS_TEMPLATE_API_AUDIT_FAIL.getCode(), ex5.getCode());
// 6. 审核成功,Assert.equals 分支
SmsTemplateRespDTO successTpl = new SmsTemplateRespDTO();
successTpl.setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus());
doReturn(successTpl).when(smsClient).getSmsTemplate(apiTemplateId);
assertDoesNotThrow(() -> smsTemplateService.validateApiTemplate(apiTemplateId));
}
}
@@ -278,8 +278,8 @@ CREATE TABLE IF NOT EXISTS "system_sms_template" (
"params" varchar(255) NOT NULL,
"remark" varchar(255) DEFAULT NULL,
"api_template_id" varchar(63) NOT NULL,
"channel_id" bigint NOT NULL,
"channel_code" varchar(63) NOT NULL,
"channel_id" bigint NULL,
"channel_code" varchar(63) NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
@@ -290,8 +290,8 @@ CREATE TABLE IF NOT EXISTS "system_sms_template" (
CREATE TABLE IF NOT EXISTS "system_sms_log" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"channel_id" bigint NOT NULL,
"channel_code" varchar(63) NOT NULL,
"channel_id" bigint NULL,
"channel_code" varchar(63) NULL,
"template_id" bigint NOT NULL,
"template_code" varchar(63) NOT NULL,
"template_type" tinyint NOT NULL,