feat(admin): 添加用户管理相关文件

添加用户管理视图、API和状态管理文件
This commit is contained in:
张翔
2026-03-28 14:37:29 +08:00
commit 08ea5fbe98
1643 changed files with 255646 additions and 0 deletions
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.destiny</groupId>
<artifactId>everything-is-suitable-api</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>everything-is-suitable-client</artifactId>
<name>Everything Is Suitable Client</name>
<description>Client for Everything Is Suitable API</description>
<dependencies>
<dependency>
<groupId>io.destiny</groupId>
<artifactId>everything-is-suitable-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>tea-openapi</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>tea-util</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,89 @@
package io.destiny.client.client;
import com.fasterxml.jackson.databind.JsonNode;
import io.destiny.client.config.DouyinMiniProgramConfig;
import io.destiny.client.dto.WechatAuthResponse;
import io.destiny.client.core.exception.ClientUserAuthExceptionCode;
import io.destiny.common.exception.ApplicationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
@Component
public class DouyinMiniProgramClient {
private static final Logger logger = LoggerFactory.getLogger(DouyinMiniProgramClient.class);
private static final String AUTH_URL = "https://developer.toutiao.com/api/apps/jscode2session";
private final WebClient webClient;
private final DouyinMiniProgramConfig config;
public DouyinMiniProgramClient(DouyinMiniProgramConfig config, WebClient webClient) {
this.config = config;
this.webClient = webClient != null ? webClient : WebClient.builder().build();
}
public Mono<WechatAuthResponse> auth(String code) {
logger.info("Douyin mini program auth request, code: {}", code);
return webClient.get()
.uri(AUTH_URL, uriBuilder -> uriBuilder
.queryParam("appid", config.getAppId())
.queryParam("secret", config.getAppSecret())
.queryParam("code", code)
.queryParam("anonymous_code", "")
.build())
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class)
.flatMap(this::parseAuthResponse)
.onErrorResume(WebClientResponseException.class, ex -> {
logger.error("Douyin auth request failed, status: {}, body: {}", ex.getStatusCode(), ex.getResponseBodyAsString());
return Mono.error(new ApplicationException(ClientUserAuthExceptionCode.CLIENT_DOUYIN_AUTH_FAILED));
})
.onErrorResume(Exception.class, ex -> {
logger.error("Douyin auth request error", ex);
return Mono.error(new ApplicationException(ClientUserAuthExceptionCode.CLIENT_DOUYIN_AUTH_FAILED));
});
}
private Mono<WechatAuthResponse> parseAuthResponse(String responseBody) {
try {
com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(responseBody);
String errNo = jsonNode.has("err_no") ? jsonNode.get("err_no").asText() : null;
if (errNo != null && !"0".equals(errNo)) {
String errMsg = jsonNode.has("err_tips") ? jsonNode.get("err_tips").asText() : "Unknown error";
logger.error("Douyin auth failed, err_no: {}, err_tips: {}", errNo, errMsg);
return Mono.error(new ApplicationException(ClientUserAuthExceptionCode.CLIENT_DOUYIN_AUTH_FAILED));
}
JsonNode dataNode = jsonNode.get("data");
if (dataNode == null) {
logger.error("Douyin auth response missing data field");
return Mono.error(new ApplicationException(ClientUserAuthExceptionCode.CLIENT_DOUYIN_AUTH_FAILED));
}
WechatAuthResponse response = new WechatAuthResponse();
response.setOpenId(dataNode.get("openid").asText());
if (dataNode.has("unionid")) {
response.setUnionId(dataNode.get("unionid").asText());
}
response.setSessionKey(dataNode.get("session_key").asText());
logger.info("Douyin auth success, openId: {}, unionId: {}", response.getOpenId(), response.getUnionId());
return Mono.just(response);
} catch (Exception e) {
logger.error("Parse Douyin auth response failed", e);
return Mono.error(new ApplicationException(ClientUserAuthExceptionCode.CLIENT_DOUYIN_AUTH_FAILED));
}
}
}
@@ -0,0 +1,83 @@
package io.destiny.client.client;
import com.fasterxml.jackson.databind.JsonNode;
import io.destiny.client.config.WechatMiniProgramConfig;
import io.destiny.client.dto.WechatAuthResponse;
import io.destiny.client.core.exception.ClientUserAuthExceptionCode;
import io.destiny.common.exception.ApplicationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
@Component
public class WechatMiniProgramClient {
private static final Logger logger = LoggerFactory.getLogger(WechatMiniProgramClient.class);
private static final String AUTH_URL = "https://api.weixin.qq.com/sns/jscode2session";
private final WebClient webClient;
private final WechatMiniProgramConfig config;
public WechatMiniProgramClient(WechatMiniProgramConfig config, WebClient webClient) {
this.config = config;
this.webClient = webClient != null ? webClient : WebClient.builder().build();
}
public Mono<WechatAuthResponse> auth(String code) {
logger.info("WeChat mini program auth request, code: {}", code);
return webClient.get()
.uri(AUTH_URL, uriBuilder -> uriBuilder
.queryParam("appid", config.getAppId())
.queryParam("secret", config.getAppSecret())
.queryParam("js_code", code)
.queryParam("grant_type", "authorization_code")
.build())
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class)
.flatMap(this::parseAuthResponse)
.onErrorResume(WebClientResponseException.class, ex -> {
logger.error("WeChat auth request failed, status: {}, body: {}", ex.getStatusCode(), ex.getResponseBodyAsString());
return Mono.error(new ApplicationException(ClientUserAuthExceptionCode.CLIENT_WECHAT_AUTH_FAILED));
})
.onErrorResume(Exception.class, ex -> {
logger.error("WeChat auth request error", ex);
return Mono.error(new ApplicationException(ClientUserAuthExceptionCode.CLIENT_WECHAT_AUTH_FAILED));
});
}
private Mono<WechatAuthResponse> parseAuthResponse(String responseBody) {
try {
com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(responseBody);
String errCode = jsonNode.has("errcode") ? jsonNode.get("errcode").asText() : null;
if (errCode != null && !"0".equals(errCode)) {
String errMsg = jsonNode.has("errmsg") ? jsonNode.get("errmsg").asText() : "Unknown error";
logger.error("WeChat auth failed, errcode: {}, errmsg: {}", errCode, errMsg);
return Mono.error(new ApplicationException(ClientUserAuthExceptionCode.CLIENT_WECHAT_AUTH_FAILED));
}
WechatAuthResponse response = new WechatAuthResponse();
response.setOpenId(jsonNode.get("openid").asText());
if (jsonNode.has("unionid")) {
response.setUnionId(jsonNode.get("unionid").asText());
}
response.setSessionKey(jsonNode.get("session_key").asText());
logger.info("WeChat auth success, openId: {}, unionId: {}", response.getOpenId(), response.getUnionId());
return Mono.just(response);
} catch (Exception e) {
logger.error("Parse WeChat auth response failed", e);
return Mono.error(new ApplicationException(ClientUserAuthExceptionCode.CLIENT_WECHAT_AUTH_FAILED));
}
}
}
@@ -0,0 +1,107 @@
package io.destiny.client.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "client.sms.aliyun")
public class AliyunSmsConfig {
private String accessKeyId;
private String accessKeySecret;
private String endpoint;
private String signName;
private String templateCode;
private Integer expireMinutes = 10;
private Integer sendIntervalSeconds = 60;
private Integer maxSendCountPerDay = 10;
private Integer maxVerifyAttempts = 5;
private Integer lockMinutes = 10;
public String getAccessKeyId() {
return accessKeyId;
}
public void setAccessKeyId(String accessKeyId) {
this.accessKeyId = accessKeyId;
}
public String getAccessKeySecret() {
return accessKeySecret;
}
public void setAccessKeySecret(String accessKeySecret) {
this.accessKeySecret = accessKeySecret;
}
public String getEndpoint() {
return endpoint;
}
public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}
public String getSignName() {
return signName;
}
public void setSignName(String signName) {
this.signName = signName;
}
public String getTemplateCode() {
return templateCode;
}
public void setTemplateCode(String templateCode) {
this.templateCode = templateCode;
}
public Integer getExpireMinutes() {
return expireMinutes;
}
public void setExpireMinutes(Integer expireMinutes) {
this.expireMinutes = expireMinutes;
}
public Integer getSendIntervalSeconds() {
return sendIntervalSeconds;
}
public void setSendIntervalSeconds(Integer sendIntervalSeconds) {
this.sendIntervalSeconds = sendIntervalSeconds;
}
public Integer getMaxSendCountPerDay() {
return maxSendCountPerDay;
}
public void setMaxSendCountPerDay(Integer maxSendCountPerDay) {
this.maxSendCountPerDay = maxSendCountPerDay;
}
public Integer getMaxVerifyAttempts() {
return maxVerifyAttempts;
}
public void setMaxVerifyAttempts(Integer maxVerifyAttempts) {
this.maxVerifyAttempts = maxVerifyAttempts;
}
public Integer getLockMinutes() {
return lockMinutes;
}
public void setLockMinutes(Integer lockMinutes) {
this.lockMinutes = lockMinutes;
}
}
@@ -0,0 +1,24 @@
package io.destiny.client.config;
public class DouyinConfig {
private String appId;
private String appSecret;
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
public String getAppSecret() {
return appSecret;
}
public void setAppSecret(String appSecret) {
this.appSecret = appSecret;
}
}
@@ -0,0 +1,27 @@
package io.destiny.client.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "client.douyin")
public class DouyinMiniProgramConfig {
private String appId;
private String appSecret;
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
public String getAppSecret() {
return appSecret;
}
public void setAppSecret(String appSecret) {
this.appSecret = appSecret;
}
}
@@ -0,0 +1,19 @@
package io.destiny.client.config;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.teaopenapi.models.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SmsClientConfig {
@Bean
public Client smsClient(AliyunSmsConfig smsConfig) throws Exception {
Config config = new Config()
.setAccessKeyId(smsConfig.getAccessKeyId())
.setAccessKeySecret(smsConfig.getAccessKeySecret())
.setEndpoint(smsConfig.getEndpoint());
return new Client(config);
}
}
@@ -0,0 +1,44 @@
package io.destiny.client.config;
public class SmsConfig {
private String accessKeyId;
private String accessKeySecret;
private String signName;
private String templateCode;
public String getAccessKeyId() {
return accessKeyId;
}
public void setAccessKeyId(String accessKeyId) {
this.accessKeyId = accessKeyId;
}
public String getAccessKeySecret() {
return accessKeySecret;
}
public void setAccessKeySecret(String accessKeySecret) {
this.accessKeySecret = accessKeySecret;
}
public String getSignName() {
return signName;
}
public void setSignName(String signName) {
this.signName = signName;
}
public String getTemplateCode() {
return templateCode;
}
public void setTemplateCode(String templateCode) {
this.templateCode = templateCode;
}
}
@@ -0,0 +1,14 @@
package io.destiny.client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient() {
return WebClient.builder().build();
}
}
@@ -0,0 +1,27 @@
package io.destiny.client.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "client.wechat")
public class WechatMiniProgramConfig {
private String appId;
private String appSecret;
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
public String getAppSecret() {
return appSecret;
}
public void setAppSecret(String appSecret) {
this.appSecret = appSecret;
}
}
@@ -0,0 +1,118 @@
package io.destiny.client.core.domain;
import java.time.LocalDateTime;
import io.destiny.common.primitive.IpAddress;
import io.destiny.common.utils.SnowflakeId;
/**
* Client Log 客户端用户登录日志
*
* @author zhangxiang
* @date 2023/12/15
*/
public class ClientLoginLog {
private Long id;
private Long clientUserId;
private IpAddress ipAddress;
private LocalDateTime loginTime;
private LocalDateTime logoutTime;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime deletedAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getClientUserId() {
return clientUserId;
}
public void setClientUserId(Long clientUserId) {
this.clientUserId = clientUserId;
}
public IpAddress getIpAddress() {
return ipAddress;
}
public void setIpAddress(IpAddress ipAddress) {
this.ipAddress = ipAddress;
}
public LocalDateTime getLoginTime() {
return loginTime;
}
public void setLoginTime(LocalDateTime loginTime) {
this.loginTime = loginTime;
}
public LocalDateTime getLogoutTime() {
return logoutTime;
}
public void setLogoutTime(LocalDateTime logoutTime) {
this.logoutTime = logoutTime;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(LocalDateTime deletedAt) {
this.deletedAt = deletedAt;
}
public ClientLoginLog() {
}
public ClientLoginLog(Long clientUserId, IpAddress ipAddress) {
this.id = SnowflakeId.nextId();
this.clientUserId = clientUserId;
this.ipAddress = ipAddress;
this.loginTime = LocalDateTime.now();
}
public void generateId() {
this.id = SnowflakeId.nextId();
}
public void delete() {
this.deletedAt = LocalDateTime.now();
}
public boolean isLogoutTimeNull() {
return this.logoutTime == null;
}
}
@@ -0,0 +1,242 @@
package io.destiny.client.core.domain;
import java.time.LocalDateTime;
import java.util.List;
import io.destiny.common.primitive.EmailAddress;
import io.destiny.common.primitive.PhoneNumber;
import io.destiny.common.utils.SnowflakeId;
/**
* Client User
*
* @author zhangxiang
* @date 2023/12/15
*/
public class ClientUser {
private Long id;
private String username;
private String password;
private EmailAddress email;
private PhoneNumber phone;
private String gender;
private LocalDateTime birthTime;
private String wechatOpenId;
private String wechatUnionId;
private String douyinOpenId;
private String douyinUnionId;
private Boolean phoneVerified;
private String nickname;
private String avatarUrl;
private String country;
private String province;
private String city;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime deletedAt;
private List<ClientLoginLog> loginLogs;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public EmailAddress getEmail() {
return email;
}
public void setEmail(EmailAddress email) {
this.email = email;
}
public PhoneNumber getPhone() {
return phone;
}
public void setPhone(PhoneNumber phone) {
this.phone = phone;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public LocalDateTime getBirthTime() {
return birthTime;
}
public void setBirthTime(LocalDateTime birthTime) {
this.birthTime = birthTime;
}
public String getWechatOpenId() {
return wechatOpenId;
}
public void setWechatOpenId(String wechatOpenId) {
this.wechatOpenId = wechatOpenId;
}
public String getWechatUnionId() {
return wechatUnionId;
}
public void setWechatUnionId(String wechatUnionId) {
this.wechatUnionId = wechatUnionId;
}
public String getDouyinOpenId() {
return douyinOpenId;
}
public void setDouyinOpenId(String douyinOpenId) {
this.douyinOpenId = douyinOpenId;
}
public String getDouyinUnionId() {
return douyinUnionId;
}
public void setDouyinUnionId(String douyinUnionId) {
this.douyinUnionId = douyinUnionId;
}
public Boolean getPhoneVerified() {
return phoneVerified;
}
public void setPhoneVerified(Boolean phoneVerified) {
this.phoneVerified = phoneVerified;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(LocalDateTime deletedAt) {
this.deletedAt = deletedAt;
}
public List<ClientLoginLog> getLoginLogs() {
return loginLogs;
}
public void setLoginLogs(List<ClientLoginLog> loginLogs) {
this.loginLogs = loginLogs;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
/**
* 生成用户ID
*/
public void generateId() {
this.id = SnowflakeId.nextId();
}
/**
* 删除用户
*/
public void delete() {
this.deletedAt = LocalDateTime.now();
}
}
@@ -0,0 +1,156 @@
package io.destiny.client.core.domain;
import java.time.LocalDateTime;
import io.destiny.common.utils.SnowflakeId;
public class SmsVerificationCode {
private Long id;
private String phone;
private String code;
private String type;
private LocalDateTime expireTime;
private Boolean verified;
private Integer verifyCount;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime deletedAt;
public SmsVerificationCode() {
}
public SmsVerificationCode(String phone, String code, String type, LocalDateTime expireTime) {
this.phone = phone;
this.code = code;
this.type = type;
this.expireTime = expireTime;
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
this.verified = false;
this.verifyCount = 0;
this.id = SnowflakeId.nextId();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public LocalDateTime getExpireTime() {
return expireTime;
}
public void setExpireTime(LocalDateTime expireTime) {
this.expireTime = expireTime;
}
public Boolean getVerified() {
return verified;
}
public void setVerified(Boolean verified) {
this.verified = verified;
}
public Integer getVerifyCount() {
return verifyCount;
}
public void setVerifyCount(Integer verifyCount) {
this.verifyCount = verifyCount;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(LocalDateTime deletedAt) {
this.deletedAt = deletedAt;
}
public void generateId() {
this.id = SnowflakeId.nextId();
}
public void delete() {
this.deletedAt = LocalDateTime.now();
}
public void markAsVerified() {
this.verified = true;
this.updatedAt = LocalDateTime.now();
}
public void incrementVerifyCount() {
if (this.verifyCount == null) {
this.verifyCount = 0;
}
this.verifyCount++;
this.updatedAt = LocalDateTime.now();
}
public boolean isExpired() {
return LocalDateTime.now().isAfter(this.expireTime);
}
public boolean isVerified() {
return Boolean.TRUE.equals(this.verified);
}
public boolean isDeleted() {
return this.deletedAt != null;
}
}
@@ -0,0 +1,125 @@
package io.destiny.client.core.domain;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import io.destiny.common.utils.SnowflakeId;
public class Subscription {
private Long id;
private Long clientUserId;
private String planType;
private LocalDate startDate;
private LocalDate endDate;
private String status;
private BigDecimal amount;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime deletedAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getClientUserId() {
return clientUserId;
}
public void setClientUserId(Long clientUserId) {
this.clientUserId = clientUserId;
}
public String getPlanType() {
return planType;
}
public void setPlanType(String planType) {
this.planType = planType;
}
public LocalDate getStartDate() {
return startDate;
}
public void setStartDate(LocalDate startDate) {
this.startDate = startDate;
}
public LocalDate getEndDate() {
return endDate;
}
public void setEndDate(LocalDate endDate) {
this.endDate = endDate;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(LocalDateTime deletedAt) {
this.deletedAt = deletedAt;
}
public void generateId() {
this.id = SnowflakeId.nextId();
}
public void delete() {
this.deletedAt = LocalDateTime.now();
}
public boolean isActive() {
return "ACTIVE".equals(status) && endDate != null && endDate.isAfter(LocalDate.now());
}
public boolean isExpired() {
return endDate != null && endDate.isBefore(LocalDate.now());
}
}
@@ -0,0 +1,108 @@
package io.destiny.client.core.domain;
import java.time.LocalDateTime;
import io.destiny.common.utils.SnowflakeId;
public class UserBinding {
private Long id;
private Long userId;
private String platform;
private String platformOpenId;
private String platformUnionId;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime deletedAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getPlatform() {
return platform;
}
public void setPlatform(String platform) {
this.platform = platform;
}
public String getPlatformOpenId() {
return platformOpenId;
}
public void setPlatformOpenId(String platformOpenId) {
this.platformOpenId = platformOpenId;
}
public String getPlatformUnionId() {
return platformUnionId;
}
public void setPlatformUnionId(String platformUnionId) {
this.platformUnionId = platformUnionId;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(LocalDateTime deletedAt) {
this.deletedAt = deletedAt;
}
public void generateId() {
this.id = SnowflakeId.nextId();
}
public void delete() {
this.deletedAt = LocalDateTime.now();
}
public boolean isDeleted() {
return this.deletedAt != null;
}
public boolean isWechatPlatform() {
return "wechat".equals(this.platform);
}
public boolean isDouyinPlatform() {
return "douyin".equals(this.platform);
}
}
@@ -0,0 +1,70 @@
package io.destiny.client.core.domain.query;
import java.time.LocalDateTime;
import io.destiny.common.primitive.IpAddress;
public class ClientLoginLogQuery {
private Long clientUserId;
private IpAddress ipAddress;
private LocalDateTime loginTimeStart;
private LocalDateTime loginTimeEnd;
private LocalDateTime logoutTimeStart;
private LocalDateTime logoutTimeEnd;
public Long getClientUserId() {
return clientUserId;
}
public void setClientUserId(Long clientUserId) {
this.clientUserId = clientUserId;
}
public IpAddress getIpAddress() {
return ipAddress;
}
public void setIpAddress(IpAddress ipAddress) {
this.ipAddress = ipAddress;
}
public LocalDateTime getLoginTimeStart() {
return loginTimeStart;
}
public void setLoginTimeStart(LocalDateTime loginTimeStart) {
this.loginTimeStart = loginTimeStart;
}
public LocalDateTime getLoginTimeEnd() {
return loginTimeEnd;
}
public void setLoginTimeEnd(LocalDateTime loginTimeEnd) {
this.loginTimeEnd = loginTimeEnd;
}
public LocalDateTime getLogoutTimeStart() {
return logoutTimeStart;
}
public void setLogoutTimeStart(LocalDateTime logoutTimeStart) {
this.logoutTimeStart = logoutTimeStart;
}
public LocalDateTime getLogoutTimeEnd() {
return logoutTimeEnd;
}
public void setLogoutTimeEnd(LocalDateTime logoutTimeEnd) {
this.logoutTimeEnd = logoutTimeEnd;
}
}
@@ -0,0 +1,39 @@
package io.destiny.client.core.domain.query;
import io.destiny.common.primitive.IpAddress;
/**
* 客户端用户登录
*/
public class ClientUserLogin {
private String username;
private String password;
private IpAddress ipAddress;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public IpAddress getIpAddress() {
return ipAddress;
}
public void setIpAddress(IpAddress ipAddress) {
this.ipAddress = ipAddress;
}
}
@@ -0,0 +1,50 @@
package io.destiny.client.core.domain.query;
import io.destiny.common.primitive.EmailAddress;
import io.destiny.common.primitive.IpAddress;
import io.destiny.common.primitive.PhoneNumber;
public class ClientUserQuery {
private PhoneNumber phone;
private String username;
private EmailAddress email;
private IpAddress ipAddress;
public PhoneNumber getPhone() {
return phone;
}
public void setPhone(PhoneNumber phone) {
this.phone = phone;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public EmailAddress getEmail() {
return email;
}
public void setEmail(EmailAddress email) {
this.email = email;
}
public IpAddress getIpAddress() {
return ipAddress;
}
public void setIpAddress(IpAddress ipAddress) {
this.ipAddress = ipAddress;
}
}
@@ -0,0 +1,36 @@
package io.destiny.client.core.enums;
import io.destiny.common.utils.EnumSupport;
public enum PlatformType implements EnumSupport {
WECHAT("wechat", "微信小程序"),
DOUYIN("douyin", "抖音小程序");
private final String code;
private final String displayName;
PlatformType(String code, String displayName) {
this.code = code;
this.displayName = displayName;
}
public String getCode() {
return code;
}
public String getDisplayName() {
return displayName;
}
public static PlatformType fromCode(String code) {
for (PlatformType type : values()) {
if (type.code.equals(code)) {
return type;
}
}
throw new IllegalArgumentException("Unknown platform type: " + code);
}
}
@@ -0,0 +1,47 @@
package io.destiny.client.core.enums;
import io.destiny.common.utils.EnumSupport;
public enum SmsCodeType implements EnumSupport {
LOGIN("login", "登录验证码", "SMS_123456789"),
REGISTER("register", "注册验证码", "SMS_123456790"),
BIND("bind", "绑定验证码", "SMS_123456791"),
RESET_PASSWORD("reset_password", "重置密码验证码", "SMS_123456792");
private final String code;
private final String displayName;
private final String templateCode;
SmsCodeType(String code, String displayName, String templateCode) {
this.code = code;
this.displayName = displayName;
this.templateCode = templateCode;
}
public String getCode() {
return code;
}
public String getDisplayName() {
return displayName;
}
public String getTemplateCode() {
return templateCode;
}
public static SmsCodeType fromCode(String code) {
for (SmsCodeType type : values()) {
if (type.code.equals(code)) {
return type;
}
}
throw new IllegalArgumentException("Unknown SMS code type: " + code);
}
}
@@ -0,0 +1,146 @@
package io.destiny.client.core.exception;
import io.destiny.common.exception.ExceptionCode;
import io.destiny.common.utils.EnumSupport;
/**
* 客户端用户认证异常码
*/
public enum ClientUserAuthExceptionCode implements ExceptionCode, EnumSupport {
/**
* 用户名或密码错误
*/
CLIENT_INVALID_USERNAME_OR_PASSWORD("401", "CLIENT_AUTH_10001", "用户名或密码错误"),
/**
* 平台标识无效
*/
CLIENT_INVALID_PLATFORM("400", "CLIENT_AUTH_10002", "平台标识无效"),
/**
* 微信授权码无效
*/
CLIENT_INVALID_WECHAT_CODE("400", "CLIENT_AUTH_10003", "微信授权码无效"),
/**
* 微信授权失败
*/
CLIENT_WECHAT_AUTH_FAILED("400", "CLIENT_AUTH_10004", "微信授权失败"),
/**
* 抖音授权码无效
*/
CLIENT_INVALID_DOUYIN_CODE("400", "CLIENT_AUTH_10005", "抖音授权码无效"),
/**
* 抖音授权失败
*/
CLIENT_DOUYIN_AUTH_FAILED("400", "CLIENT_AUTH_10006", "抖音授权失败"),
/**
* 验证码已过期
*/
CLIENT_SMS_CODE_EXPIRED("400", "CLIENT_AUTH_10007", "验证码已过期"),
/**
* 验证码错误
*/
CLIENT_SMS_CODE_INVALID("400", "CLIENT_AUTH_10008", "验证码错误"),
/**
* 验证码已使用
*/
CLIENT_SMS_CODE_USED("400", "CLIENT_AUTH_10009", "验证码已使用"),
/**
* 验证码验证次数过多
*/
CLIENT_SMS_CODE_EXCEEDED("400", "CLIENT_AUTH_10010", "验证码验证次数过多"),
/**
* 验证码发送频率过高
*/
CLIENT_SMS_CODE_SEND_TOO_FAST("429", "CLIENT_AUTH_10011", "验证码发送频率过高"),
/**
* 验证码发送次数超限
*/
CLIENT_SMS_CODE_SEND_EXCEEDED("429", "CLIENT_AUTH_10012", "验证码发送次数超限"),
/**
* 用户不存在
*/
CLIENT_USER_NOT_FOUND("404", "CLIENT_AUTH_10013", "用户不存在"),
/**
* 账号已绑定
*/
CLIENT_ACCOUNT_ALREADY_BOUND("400", "CLIENT_AUTH_10014", "账号已绑定"),
/**
* 账号绑定失败
*/
CLIENT_ACCOUNT_BIND_FAILED("400", "CLIENT_AUTH_10015", "账号绑定失败"),
/**
* 手机号未验证
*/
CLIENT_PHONE_NOT_VERIFIED("400", "CLIENT_AUTH_10016", "手机号未验证"),
/**
* 自动注册失败
*/
CLIENT_AUTO_REGISTER_FAILED("400", "CLIENT_AUTH_10017", "自动注册失败"),
/**
* 登录类型无效
*/
CLIENT_LOGIN_TYPE_INVALID("400", "CLIENT_AUTH_10018", "登录类型无效"),
/**
* 登录失败
*/
CLIENT_LOGIN_FAILED("401", "CLIENT_AUTH_10019", "登录失败"),
/**
* 账号已锁定
*/
CLIENT_ACCOUNT_LOCKED("403", "CLIENT_AUTH_10020", "账号已锁定");
/**
* HTTP状态码
*/
private final String httpCode;
/**
* 业务错误码
*/
private final String businessCode;
/**
* 异常码显示名称
*/
private final String displayName;
ClientUserAuthExceptionCode(String httpCode, String businessCode, String displayName) {
this.httpCode = httpCode;
this.businessCode = businessCode;
this.displayName = displayName;
}
@Override
public String getHttpCode() {
return httpCode;
}
@Override
public String getCode() {
return businessCode;
}
@Override
public String getDisplayName() {
return displayName;
}
}
@@ -0,0 +1,42 @@
package io.destiny.client.core.exception;
import io.destiny.common.exception.ExceptionCode;
import io.destiny.common.utils.EnumSupport;
/**
* 客户端用户异常码
*/
public enum ClientUserExceptionCode implements ExceptionCode, EnumSupport {
/**
* 客户端用户不存在
*/
CLIENT_USER_NOT_FOUND("500", "CLIENT_USER_20001", "客户端用户不存在");
private final String httpCode;
private final String businessCode;
private final String displayName;
ClientUserExceptionCode(String httpCode, String businessCode, String displayName) {
this.httpCode = httpCode;
this.businessCode = businessCode;
this.displayName = displayName;
}
@Override
public String getHttpCode() {
return httpCode;
}
@Override
public String getCode() {
return businessCode;
}
@Override
public String getDisplayName() {
return displayName;
}
}
@@ -0,0 +1,87 @@
package io.destiny.client.core.exception;
import io.destiny.common.exception.ApplicationException;
import io.destiny.common.response.Result;
import io.destiny.common.utils.HttpStatusUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
public Mono<ServerResponse> handleException(Throwable ex, ServerWebExchange exchange) {
if (ex instanceof ApplicationException) {
return handleApplicationException((ApplicationException) ex, exchange);
} else if (ex instanceof IllegalArgumentException) {
return handleIllegalArgumentException((IllegalArgumentException) ex, exchange);
} else if (ex instanceof Exception) {
return handleGenericException((Exception) ex, exchange);
} else {
return handleUnknownException(ex, exchange);
}
}
private Mono<ServerResponse> handleApplicationException(ApplicationException ex, ServerWebExchange exchange) {
String httpCode = ex.getHttpCode();
String businessCode = ex.getExceptionCode() != null ? ex.getExceptionCode().getCode() : "ERROR";
String message = ex.getExceptionMessage() != null ? ex.getExceptionMessage() :
(ex.getExceptionCode() != null ? ex.getExceptionCode().getDisplayName() : "系统异常");
HttpStatus status = HttpStatusUtils.getHttpStatusByCode(httpCode);
String path = exchange != null ? exchange.getRequest().getPath().value() : "unknown";
logger.warn("ApplicationException - HTTP: {}, Business: {}, Message: {}, Path: {}",
httpCode, businessCode, message, path);
Result<Void> result = Result.error(businessCode, message);
return ServerResponse.status(status)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(result);
}
private Mono<ServerResponse> handleIllegalArgumentException(IllegalArgumentException ex,
ServerWebExchange exchange) {
String path = exchange != null ? exchange.getRequest().getPath().value() : "unknown";
logger.warn("IllegalArgumentException - Message: {}, Path: {}",
ex.getMessage(), path);
Result<Void> result = Result.error("INVALID_ARGUMENT", ex.getMessage());
return ServerResponse.badRequest()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(result);
}
private Mono<ServerResponse> handleGenericException(Exception ex, ServerWebExchange exchange) {
String path = exchange != null ? exchange.getRequest().getPath().value() : "unknown";
logger.error("Unexpected exception - Message: {}, Path: {}",
ex.getMessage(), path, ex);
Result<Void> result = Result.error("INTERNAL_ERROR", "Internal server error");
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(result);
}
private Mono<ServerResponse> handleUnknownException(Throwable ex, ServerWebExchange exchange) {
String path = exchange != null ? exchange.getRequest().getPath().value() : "unknown";
logger.error("Unknown exception - Message: {}, Path: {}",
ex.getMessage(), path, ex);
Result<Void> result = Result.error("UNKNOWN_ERROR", "Unknown error occurred");
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(result);
}
}
@@ -0,0 +1,17 @@
package io.destiny.client.core.repository;
import io.destiny.client.core.domain.ClientLoginLog;
import io.destiny.client.core.domain.query.ClientLoginLogQuery;
import io.destiny.common.request.PageQuery;
import io.destiny.common.response.PageModel;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface IClientLoginLogRepository {
Mono<PageModel<ClientLoginLog>> findByQueryWithPagination(ClientLoginLogQuery query, PageQuery pageQuery);
Mono<ClientLoginLog> save(ClientLoginLog loginLog);
Flux<ClientLoginLog> findByClientUserId(Long clientUserId);
}
@@ -0,0 +1,38 @@
package io.destiny.client.core.repository;
import io.destiny.client.core.domain.ClientUser;
import io.destiny.client.core.domain.query.ClientUserQuery;
import io.destiny.common.primitive.PhoneNumber;
import io.destiny.common.request.PageQuery;
import io.destiny.common.response.PageModel;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 客户端用户仓库接口
*
* @author zhangxiang
* @date 2025-12-30
*/
public interface IClientUserRepository {
Mono<ClientUser> findByPhone(PhoneNumber phone);
Mono<ClientUser> findByWechatOpenId(String wechatOpenId);
Mono<ClientUser> findByDouyinOpenId(String douyinOpenId);
Mono<ClientUser> findByWechatUnionId(String wechatUnionId);
Mono<ClientUser> findByDouyinUnionId(String douyinUnionId);
Mono<ClientUser> save(ClientUser clientUser);
Mono<ClientUser> findById(Long id);
Mono<ClientUser> findByUsername(String username);
Flux<ClientUser> findByQuery(ClientUserQuery query);
Mono<PageModel<ClientUser>> findByQueryWithPagination(ClientUserQuery query, PageQuery pageQuery);
}
@@ -0,0 +1,19 @@
package io.destiny.client.core.repository;
import io.destiny.client.core.domain.SmsVerificationCode;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
public interface ISmsVerificationCodeRepository {
Mono<SmsVerificationCode> save(SmsVerificationCode smsVerificationCode);
Mono<SmsVerificationCode> findLatestByPhoneAndType(String phone, String type);
Mono<Long> countByPhoneAndCreatedAtAfter(String phone, LocalDateTime createdAt);
Mono<Integer> deleteExpiredBefore(LocalDateTime expireTime);
Mono<Void> markAsVerified(Long id);
}
@@ -0,0 +1,20 @@
package io.destiny.client.core.repository;
import io.destiny.client.core.domain.Subscription;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface ISubscriptionRepository {
Flux<Subscription> findAll();
Flux<Subscription> findByStatus(String status);
Flux<Subscription> findByClientUserId(Long clientUserId);
Flux<Subscription> findByStartDateBetween(long startTimestamp, long endTimestamp);
Mono<Subscription> save(Subscription subscription);
Mono<Void> deleteById(Long id);
}
@@ -0,0 +1,20 @@
package io.destiny.client.core.repository;
import io.destiny.client.core.domain.UserBinding;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface IUserBindingRepository {
Mono<UserBinding> save(UserBinding userBinding);
Mono<UserBinding> findByUserIdAndPlatform(Long userId, String platform);
Mono<UserBinding> findByPlatformAndOpenId(String platform, String platformOpenId);
Flux<UserBinding> findByUserId(Long userId);
Mono<UserBinding> findByPlatformAndUnionId(String platform, String platformUnionId);
Mono<Void> deleteByUserIdAndPlatform(Long userId, String platform);
}
@@ -0,0 +1,108 @@
package io.destiny.client.core.service;
import io.destiny.client.core.domain.ClientLoginLog;
import io.destiny.client.core.domain.ClientUser;
import io.destiny.client.core.domain.query.ClientUserLogin;
import io.destiny.client.core.repository.IClientLoginLogRepository;
import io.destiny.client.core.repository.IClientUserRepository;
import io.destiny.client.dto.ClientLoginResponse;
import io.destiny.client.security.JwtTokenProvider;
import io.destiny.common.exception.ApplicationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import static io.destiny.client.core.exception.ClientUserAuthExceptionCode.CLIENT_INVALID_USERNAME_OR_PASSWORD;
@Service
public class ClientUserAuthService {
private final IClientUserRepository clientUserRepository;
private final IClientLoginLogRepository clientLoginLogRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
public ClientUserAuthService(IClientUserRepository clientUserRepository,
IClientLoginLogRepository clientLoginLogRepository, PasswordEncoder passwordEncoder,
JwtTokenProvider jwtTokenProvider) {
this.clientUserRepository = clientUserRepository;
this.clientLoginLogRepository = clientLoginLogRepository;
this.passwordEncoder = passwordEncoder;
this.jwtTokenProvider = jwtTokenProvider;
}
/**
* 注册客户端用户
*/
public Mono<ClientUser> register(ClientUser clientUser) {
return Mono.fromCallable(() -> passwordEncoder.encode(clientUser.getPassword()))
.map(encodedPassword -> {
ClientUser newUser = new ClientUser();
newUser.setUsername(clientUser.getUsername());
newUser.setPassword(encodedPassword);
newUser.setEmail(clientUser.getEmail());
newUser.setPhone(clientUser.getPhone());
newUser.setGender(clientUser.getGender());
newUser.setBirthTime(clientUser.getBirthTime());
return newUser;
})
.flatMap(clientUserRepository::save);
}
/**
* 登录并记录登录日志
*/
public Mono<ClientLoginResponse> login(ClientUserLogin clientUserLogin) {
return clientUserRepository.findByUsername(clientUserLogin.getUsername())
.filter(user -> passwordEncoder.matches(clientUserLogin.getPassword(), user.getPassword()))
.switchIfEmpty(Mono.error(new ApplicationException(CLIENT_INVALID_USERNAME_OR_PASSWORD)))
.flatMap(user -> clientLoginLogRepository
.save(new ClientLoginLog(user.getId(), clientUserLogin.getIpAddress()))
.thenReturn(user))
.map(user -> {
String token = jwtTokenProvider.generateToken(user.getId(), user.getUsername());
return ClientLoginResponse.from(token, user);
});
}
/**
* 刷新登录令牌
*/
public Mono<ClientLoginResponse> refreshLoginToken(String oldToken) {
return Mono.fromCallable(() -> jwtTokenProvider.getUserIdFromToken(oldToken))
.switchIfEmpty(Mono.error(new ApplicationException(CLIENT_INVALID_USERNAME_OR_PASSWORD)))
.flatMap(userId -> clientUserRepository.findById(userId))
.switchIfEmpty(Mono.error(new ApplicationException(CLIENT_INVALID_USERNAME_OR_PASSWORD)))
.map(user -> {
String newToken = jwtTokenProvider.generateToken(user.getId(), user.getUsername());
return ClientLoginResponse.from(newToken, user);
});
}
/**
* 注销登录
*/
public Mono<Void> logout(Long clientUserId) {
return clientLoginLogRepository.findByClientUserId(clientUserId)
.filter(ClientLoginLog::isLogoutTimeNull)
.next()
.flatMap(loginLog -> {
loginLog.setLogoutTime(LocalDateTime.now());
return clientLoginLogRepository.save(loginLog);
})
.then();
}
/**
* 检查登录状态
*/
public Mono<Boolean> checkLoginStatus(Long clientUserId) {
return clientLoginLogRepository.findByClientUserId(clientUserId)
.filter(ClientLoginLog::isLogoutTimeNull)
.hasElements()
.defaultIfEmpty(false);
}
}
@@ -0,0 +1,84 @@
package io.destiny.client.core.service;
import org.springframework.stereotype.Service;
import io.destiny.client.core.domain.ClientUser;
import io.destiny.client.core.domain.query.ClientUserQuery;
import io.destiny.client.core.repository.IClientUserRepository;
import io.destiny.common.exception.ApplicationException;
import io.destiny.common.request.PageQuery;
import io.destiny.common.response.PageModel;
import reactor.core.publisher.Mono;
import static io.destiny.client.core.exception.ClientUserExceptionCode.CLIENT_USER_NOT_FOUND;
@Service
public class ClientUserService {
private final IClientUserRepository clientUserRepository;
public ClientUserService(IClientUserRepository clientUserRepository) {
this.clientUserRepository = clientUserRepository;
}
/**
* 保存客户端用户
*
* @param clientUser 客户端用户
* @return 保存后的客户端用户
*/
public Mono<ClientUser> save(ClientUser clientUser) {
return clientUserRepository.save(clientUser);
}
/**
* 更新客户端用户
*
* @param clientUser 客户端用户
* @return 更新后的客户端用户
*/
public Mono<ClientUser> update(ClientUser clientUser) {
return clientUserRepository.findById(clientUser.getId())
.switchIfEmpty(Mono.error(new ApplicationException(CLIENT_USER_NOT_FOUND)))
.flatMap(existingClientUser -> {
clientUser.setCreatedAt(existingClientUser.getCreatedAt());
return clientUserRepository.save(clientUser);
});
}
/**
* 删除客户端用户
*
* @param id 客户端用户ID
* @return 删除结果
*/
public Mono<Void> deleteById(Long id) {
return clientUserRepository.findById(id)
.switchIfEmpty(Mono.error(new ApplicationException(CLIENT_USER_NOT_FOUND)))
.flatMap(existingClientUser -> {
existingClientUser.delete();
return clientUserRepository.save(existingClientUser);
})
.then();
}
/**
* 根据客户端用户ID查询客户端用户
*
* @param id 客户端用户ID
* @return 客户端用户
*/
public Mono<ClientUser> findById(Long id) {
return clientUserRepository.findById(id);
}
/**
* 分页查询客户端用户
*
* @param pageRequest 分页请求
* @return 客户端用户分页
*/
public Mono<PageModel<ClientUser>> findByPage(ClientUserQuery query, PageQuery pageRequest) {
return clientUserRepository.findByQueryWithPagination(query, pageRequest);
}
}
@@ -0,0 +1,200 @@
package io.destiny.client.core.service;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.destiny.client.config.AliyunSmsConfig;
import io.destiny.client.core.domain.SmsVerificationCode;
import io.destiny.client.core.enums.SmsCodeType;
import io.destiny.client.core.exception.ClientUserAuthExceptionCode;
import io.destiny.client.core.repository.ISmsVerificationCodeRepository;
import io.destiny.common.exception.ApplicationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@Service
public class SmsService {
private static final Logger logger = LoggerFactory.getLogger(SmsService.class);
private static final int CODE_EXPIRE_MINUTES = 10;
private static final int MAX_SEND_COUNT_PER_MINUTE = 1;
private static final int MAX_SEND_COUNT_PER_DAY = 10;
private static final Random RANDOM = new Random();
private final AliyunSmsConfig smsConfig;
private final ISmsVerificationCodeRepository smsVerificationCodeRepository;
private final Client smsClient;
private final Cache<String, SendRecord> sendRecords;
public SmsService(AliyunSmsConfig smsConfig, ISmsVerificationCodeRepository smsVerificationCodeRepository,
Client smsClient) {
this.smsConfig = smsConfig;
this.smsVerificationCodeRepository = smsVerificationCodeRepository;
this.smsClient = smsClient;
this.sendRecords = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.DAYS)
.recordStats()
.build();
}
public Mono<String> sendVerificationCode(String phone, SmsCodeType type) {
if (phone == null || phone.isEmpty()) {
return Mono.error(new ApplicationException(ClientUserAuthExceptionCode.CLIENT_SMS_CODE_INVALID));
}
if (type == null) {
return Mono.error(new ApplicationException(ClientUserAuthExceptionCode.CLIENT_SMS_CODE_SEND_EXCEEDED));
}
return checkSendRateLimit(phone)
.then(generateVerificationCode())
.flatMap(code -> sendSms(phone, code, type.getTemplateCode())
.flatMap(response -> saveVerificationCode(phone, code, type))
.thenReturn(code));
}
private Mono<Void> checkSendRateLimit(String phone) {
if (phone == null || phone.isEmpty()) {
return Mono.error(new ApplicationException(ClientUserAuthExceptionCode.CLIENT_SMS_CODE_INVALID));
}
SendRecord record = sendRecords.getIfPresent(phone);
if (record != null) {
LocalDateTime now = LocalDateTime.now();
if (record.getMinuteCount() >= MAX_SEND_COUNT_PER_MINUTE) {
if (now.isBefore(record.getMinuteExpireTime())) {
logger.warn("SMS send rate limit exceeded (1 minute) - Phone: {}", phone);
return Mono
.error(new ApplicationException(ClientUserAuthExceptionCode.CLIENT_SMS_CODE_SEND_TOO_FAST));
} else {
record.resetMinuteCount();
}
}
if (record.getDayCount() >= MAX_SEND_COUNT_PER_DAY) {
if (now.isBefore(record.getDayExpireTime())) {
logger.warn("SMS send rate limit exceeded (1 day) - Phone: {}", phone);
return Mono
.error(new ApplicationException(ClientUserAuthExceptionCode.CLIENT_SMS_CODE_SEND_EXCEEDED));
} else {
record.resetDayCount();
}
}
}
return Mono.empty();
}
private Mono<String> generateVerificationCode() {
int code = 100000 + RANDOM.nextInt(900000);
return Mono.just(String.valueOf(code));
}
private Mono<SendSmsResponse> sendSms(String phone, String code, String templateCode) {
return Mono.fromCallable(() -> {
String signName = smsConfig.getSignName();
if (signName == null) {
throw new ApplicationException(ClientUserAuthExceptionCode.CLIENT_SMS_CODE_SEND_EXCEEDED);
}
SendSmsRequest request = new SendSmsRequest()
.setPhoneNumbers(phone)
.setSignName(signName)
.setTemplateCode(templateCode)
.setTemplateParam("{\"code\":\"" + code + "\"}");
logger.info("Sending SMS - Phone: {}, Template: {}, Code: {}", phone, templateCode, code);
SendSmsResponse response = smsClient.sendSms(request);
if (!"OK".equals(response.getBody().getCode())) {
logger.error("SMS send failed - Phone: {}, Code: {}, Message: {}",
phone, response.getBody().getCode(), response.getBody().getMessage());
throw new ApplicationException(ClientUserAuthExceptionCode.CLIENT_SMS_CODE_SEND_EXCEEDED);
}
logger.info("SMS sent successfully - Phone: {}, RequestId: {}",
phone, response.getBody().getRequestId());
return response;
});
}
private Mono<SmsVerificationCode> saveVerificationCode(String phone, String code, SmsCodeType type) {
String typeCode = type != null ? type.getCode() : null;
return smsVerificationCodeRepository.save(new SmsVerificationCode(
phone,
code,
typeCode,
LocalDateTime.now().plusMinutes(CODE_EXPIRE_MINUTES))).doOnSuccess(v -> updateSendRecord(phone));
}
private void updateSendRecord(String phone) {
if (phone == null || phone.isEmpty()) {
return;
}
try {
SendRecord record = sendRecords.get(phone, key -> new SendRecord());
record.increment();
} catch (Exception e) {
logger.error("Failed to update send record for phone: {}", phone, e);
}
}
private static class SendRecord {
private int minuteCount = 0;
private LocalDateTime minuteExpireTime;
private int dayCount = 0;
private LocalDateTime dayExpireTime;
public void increment() {
LocalDateTime now = LocalDateTime.now();
minuteCount++;
dayCount++;
if (minuteExpireTime == null) {
minuteExpireTime = now.plusMinutes(1);
}
if (dayExpireTime == null) {
dayExpireTime = now.plusDays(1);
}
}
public void resetMinuteCount() {
minuteCount = 0;
minuteExpireTime = LocalDateTime.now().plusMinutes(1);
}
public void resetDayCount() {
dayCount = 0;
dayExpireTime = LocalDateTime.now().plusDays(1);
}
public int getMinuteCount() {
return minuteCount;
}
public int getDayCount() {
return dayCount;
}
public LocalDateTime getMinuteExpireTime() {
return minuteExpireTime;
}
public LocalDateTime getDayExpireTime() {
return dayExpireTime;
}
}
}
@@ -0,0 +1,42 @@
package io.destiny.client.core.strategy;
import io.destiny.client.dto.LoginRequest;
import io.destiny.client.dto.LoginResult;
import io.destiny.client.core.service.ClientUserAuthService;
import io.destiny.client.security.JwtTokenProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
public abstract class AbstractLoginStrategy implements LoginStrategy {
protected final Logger logger = LoggerFactory.getLogger(getClass());
protected final ClientUserAuthService clientUserAuthService;
protected final JwtTokenProvider jwtTokenProvider;
public AbstractLoginStrategy(ClientUserAuthService clientUserAuthService, JwtTokenProvider jwtTokenProvider) {
this.clientUserAuthService = clientUserAuthService;
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
public Mono<LoginResult> login(LoginRequest request) {
return validateRequest(request)
.flatMap(this::doLogin)
.flatMap(this::generateToken)
.doOnSuccess(result -> logger.info("Login successful, userId: {}, platform: {}", result.getUserId(), result.getPlatform()))
.doOnError(error -> logger.error("Login failed, error: {}", error.getMessage()));
}
protected abstract Mono<LoginRequest> validateRequest(LoginRequest request);
protected abstract Mono<LoginResult> doLogin(LoginRequest request);
protected Mono<LoginResult> generateToken(LoginResult result) {
String token = jwtTokenProvider.generateToken(result.getUserId(), result.getPlatform());
result.setToken(token);
return Mono.just(result);
}
}
@@ -0,0 +1,115 @@
package io.destiny.client.core.strategy;
import io.destiny.client.client.DouyinMiniProgramClient;
import io.destiny.client.core.domain.ClientUser;
import io.destiny.client.core.domain.UserBinding;
import io.destiny.client.core.repository.IClientUserRepository;
import io.destiny.client.core.repository.IUserBindingRepository;
import io.destiny.client.core.service.ClientUserAuthService;
import io.destiny.client.dto.DouyinLoginRequest;
import io.destiny.client.dto.LoginRequest;
import io.destiny.client.dto.LoginResult;
import io.destiny.client.security.JwtTokenProvider;
import io.destiny.common.exception.ApplicationException;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import static io.destiny.client.core.exception.ClientUserAuthExceptionCode.*;
@Component
public class DouyinLoginStrategy extends AbstractLoginStrategy {
private static final String LOGIN_TYPE = "douyin";
private final DouyinMiniProgramClient douyinClient;
private final IClientUserRepository clientUserRepository;
private final IUserBindingRepository userBindingRepository;
public DouyinLoginStrategy(ClientUserAuthService clientUserAuthService,
JwtTokenProvider jwtTokenProvider,
DouyinMiniProgramClient douyinClient,
IClientUserRepository clientUserRepository,
IUserBindingRepository userBindingRepository) {
super(clientUserAuthService, jwtTokenProvider);
this.douyinClient = douyinClient;
this.clientUserRepository = clientUserRepository;
this.userBindingRepository = userBindingRepository;
}
@Override
public String getLoginType() {
return LOGIN_TYPE;
}
@Override
protected Mono<LoginRequest> validateRequest(LoginRequest request) {
if (request.getData() instanceof DouyinLoginRequest) {
DouyinLoginRequest douyinRequest = (DouyinLoginRequest) request.getData();
if (douyinRequest.getCode() == null || douyinRequest.getCode().isEmpty()) {
return Mono.error(new ApplicationException(CLIENT_INVALID_DOUYIN_CODE));
}
return Mono.just(request);
}
return Mono.error(new ApplicationException(CLIENT_INVALID_DOUYIN_CODE));
}
@Override
protected Mono<LoginResult> doLogin(LoginRequest request) {
DouyinLoginRequest douyinRequest = (DouyinLoginRequest) request.getData();
return douyinClient.auth(douyinRequest.getCode())
.flatMap(authResponse -> {
String openId = authResponse.getOpenId();
String unionId = authResponse.getUnionId();
return clientUserRepository.findByDouyinOpenId(openId)
.switchIfEmpty(Mono.defer(() -> {
if (unionId != null && !unionId.isEmpty()) {
return clientUserRepository.findByDouyinUnionId(unionId);
}
return Mono.empty();
}))
.switchIfEmpty(Mono.defer(() -> {
if (unionId != null && !unionId.isEmpty()) {
return userBindingRepository.findByPlatformAndUnionId("douyin", unionId)
.flatMap(binding -> clientUserRepository.findById(binding.getUserId()));
}
return Mono.empty();
}))
.switchIfEmpty(Mono.defer(() -> createNewUser(openId, unionId)))
.map(user -> buildLoginResult(user, "douyin"));
});
}
private Mono<ClientUser> createNewUser(String openId, String unionId) {
ClientUser newUser = new ClientUser();
newUser.generateId();
newUser.setDouyinOpenId(openId);
if (unionId != null && !unionId.isEmpty()) {
newUser.setDouyinUnionId(unionId);
}
newUser.setPhoneVerified(false);
return clientUserRepository.save(newUser)
.flatMap(user -> {
UserBinding binding = new UserBinding();
binding.generateId();
binding.setUserId(user.getId());
binding.setPlatform("douyin");
binding.setPlatformOpenId(openId);
if (unionId != null && !unionId.isEmpty()) {
binding.setPlatformUnionId(unionId);
}
return userBindingRepository.save(binding).thenReturn(user);
});
}
private LoginResult buildLoginResult(ClientUser user, String platform) {
LoginResult result = new LoginResult();
result.setUserId(user.getId());
result.setUsername(user.getUsername());
result.setPhone(user.getPhone() != null ? user.getPhone().getValue() : null);
result.setPlatform(platform);
return result;
}
}
@@ -0,0 +1,12 @@
package io.destiny.client.core.strategy;
import io.destiny.client.dto.LoginRequest;
import io.destiny.client.dto.LoginResult;
import reactor.core.publisher.Mono;
public interface LoginStrategy {
Mono<LoginResult> login(LoginRequest request);
String getLoginType();
}
@@ -0,0 +1,58 @@
package io.destiny.client.core.strategy;
import io.destiny.client.core.exception.ClientUserAuthExceptionCode;
import io.destiny.common.exception.ApplicationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
@Component
public class LoginStrategyFactory {
private static final Logger logger = LoggerFactory.getLogger(LoginStrategyFactory.class);
private final Map<String, LoginStrategy> strategyMap;
private final List<LoginStrategy> strategies;
public LoginStrategyFactory(List<LoginStrategy> strategies) {
this.strategyMap = new ConcurrentHashMap<>();
this.strategies = strategies;
}
@PostConstruct
public void init() {
this.strategyMap.putAll(strategies.stream()
.collect(Collectors.toMap(
strategy -> strategy.getLoginType().toLowerCase(),
Function.identity()
)));
logger.info("Initialized LoginStrategyFactory with strategies: {}", strategyMap.keySet());
}
public LoginStrategy getStrategy(String loginType) {
if (loginType == null || loginType.isEmpty()) {
logger.error("Login type is null or empty");
throw new ApplicationException(ClientUserAuthExceptionCode.CLIENT_LOGIN_TYPE_INVALID);
}
LoginStrategy strategy = strategyMap.get(loginType.toLowerCase());
if (strategy == null) {
logger.error("Unsupported login type: {}", loginType);
throw new ApplicationException(ClientUserAuthExceptionCode.CLIENT_LOGIN_TYPE_INVALID);
}
logger.debug("Retrieved strategy for login type: {}", loginType);
return strategy;
}
public boolean supports(String loginType) {
return loginType != null && strategyMap.containsKey(loginType.toLowerCase());
}
}
@@ -0,0 +1,160 @@
package io.destiny.client.core.strategy;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.destiny.client.core.exception.ClientUserAuthExceptionCode;
import io.destiny.client.dto.LoginRequest;
import io.destiny.client.dto.LoginResult;
import io.destiny.common.exception.ApplicationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.concurrent.TimeUnit;
@Component
public class LoginStrategyProxy {
private static final Logger logger = LoggerFactory.getLogger(LoginStrategyProxy.class);
private final LoginStrategyFactory strategyFactory;
private static final int MAX_LOGIN_ATTEMPTS = 5;
private static final long LOCK_TIME_MS = 10 * 60 * 1000;
private final Cache<String, LoginAttemptRecord> attemptRecords;
public LoginStrategyProxy(LoginStrategyFactory strategyFactory) {
this.strategyFactory = strategyFactory;
this.attemptRecords = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats()
.build();
}
public Mono<LoginResult> login(LoginRequest request) {
String loginType = request.getLoginType();
String clientKey = getClientKey(request);
String ipAddress = request.getIpAddress() != null ? request.getIpAddress() : "unknown";
logger.info("Login request received - Type: {}, IP: {}, ClientKey: {}",
loginType, ipAddress, clientKey);
long startTime = System.currentTimeMillis();
return checkRateLimit(clientKey)
.then(Mono.defer(() -> {
LoginStrategy strategy = strategyFactory.getStrategy(loginType);
return strategy.login(request);
}))
.doOnSuccess(result -> {
long duration = System.currentTimeMillis() - startTime;
logger.info("Login successful - Type: {}, UserId: {}, Duration: {}ms",
loginType, result.getUserId(), duration);
clearAttemptRecord(clientKey);
})
.doOnError(error -> {
long duration = System.currentTimeMillis() - startTime;
logger.error("Login failed - Type: {}, Error: {}, Duration: {}ms",
loginType, error.getMessage(), duration);
recordFailedAttempt(clientKey);
})
.onErrorResume(error -> {
if (error instanceof ApplicationException) {
return Mono.error(error);
}
logger.error("Unexpected error during login", error);
return Mono.error(new ApplicationException(ClientUserAuthExceptionCode.CLIENT_LOGIN_FAILED));
});
}
private Mono<Void> checkRateLimit(String clientKey) {
if (clientKey == null) {
return Mono.empty();
}
LoginAttemptRecord record = attemptRecords.getIfPresent(clientKey);
if (record != null && record.isLocked()) {
long remainingTime = (record.getLockUntil() - System.currentTimeMillis()) / 1000;
logger.warn("Login attempt blocked - ClientKey: {}, Remaining lock time: {}s",
clientKey, remainingTime);
return Mono.error(new ApplicationException(ClientUserAuthExceptionCode.CLIENT_ACCOUNT_LOCKED));
}
return Mono.empty();
}
private void recordFailedAttempt(String clientKey) {
if (clientKey == null) {
return;
}
try {
LoginAttemptRecord record = attemptRecords.get(clientKey, key -> new LoginAttemptRecord());
record.incrementAttemptCount();
if (record.getAttemptCount() >= MAX_LOGIN_ATTEMPTS) {
record.lock();
logger.warn("Account locked due to too many failed attempts - ClientKey: {}, Attempts: {}",
clientKey, record.getAttemptCount());
}
} catch (Exception e) {
logger.error("Failed to record failed attempt for clientKey: {}", clientKey, e);
}
}
private void clearAttemptRecord(String clientKey) {
if (clientKey != null) {
attemptRecords.invalidate(clientKey);
}
}
private String getClientKey(LoginRequest request) {
String loginType = request.getLoginType() != null ? request.getLoginType() : "unknown";
String ipAddress = request.getIpAddress() != null ? request.getIpAddress() : "unknown";
if (request.getData() instanceof io.destiny.client.dto.WechatLoginRequest) {
io.destiny.client.dto.WechatLoginRequest wechatRequest = (io.destiny.client.dto.WechatLoginRequest) request
.getData();
String code = wechatRequest.getCode() != null ? wechatRequest.getCode() : "unknown";
return loginType + ":" + code + ":" + ipAddress;
} else if (request.getData() instanceof io.destiny.client.dto.DouyinLoginRequest) {
io.destiny.client.dto.DouyinLoginRequest douyinRequest = (io.destiny.client.dto.DouyinLoginRequest) request
.getData();
String code = douyinRequest.getCode() != null ? douyinRequest.getCode() : "unknown";
return loginType + ":" + code + ":" + ipAddress;
} else if (request.getData() instanceof io.destiny.client.dto.SmsLoginRequest) {
io.destiny.client.dto.SmsLoginRequest smsRequest = (io.destiny.client.dto.SmsLoginRequest) request
.getData();
String phone = smsRequest.getPhone() != null ? smsRequest.getPhone() : "unknown";
return loginType + ":" + phone + ":" + ipAddress;
}
return loginType + ":" + ipAddress;
}
private static class LoginAttemptRecord {
private int attemptCount = 0;
private volatile long lockUntil = 0;
public int getAttemptCount() {
return attemptCount;
}
public void incrementAttemptCount() {
attemptCount++;
}
public void lock() {
lockUntil = System.currentTimeMillis() + LOCK_TIME_MS;
}
public boolean isLocked() {
return lockUntil > System.currentTimeMillis();
}
public long getLockUntil() {
return lockUntil;
}
}
}
@@ -0,0 +1,108 @@
package io.destiny.client.core.strategy;
import io.destiny.client.core.domain.ClientUser;
import io.destiny.client.core.enums.SmsCodeType;
import io.destiny.client.core.repository.IClientUserRepository;
import io.destiny.client.core.repository.ISmsVerificationCodeRepository;
import io.destiny.client.core.service.ClientUserAuthService;
import io.destiny.client.dto.LoginRequest;
import io.destiny.client.dto.LoginResult;
import io.destiny.client.dto.SmsLoginRequest;
import io.destiny.client.security.JwtTokenProvider;
import io.destiny.common.exception.ApplicationException;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import static io.destiny.client.core.exception.ClientUserAuthExceptionCode.*;
@Component
public class SmsLoginStrategy extends AbstractLoginStrategy {
private static final String LOGIN_TYPE = "sms";
private static final int MAX_VERIFY_COUNT = 5;
private final IClientUserRepository clientUserRepository;
private final ISmsVerificationCodeRepository smsVerificationCodeRepository;
public SmsLoginStrategy(ClientUserAuthService clientUserAuthService,
JwtTokenProvider jwtTokenProvider,
IClientUserRepository clientUserRepository,
ISmsVerificationCodeRepository smsVerificationCodeRepository) {
super(clientUserAuthService, jwtTokenProvider);
this.clientUserRepository = clientUserRepository;
this.smsVerificationCodeRepository = smsVerificationCodeRepository;
}
@Override
public String getLoginType() {
return LOGIN_TYPE;
}
@Override
protected Mono<LoginRequest> validateRequest(LoginRequest request) {
if (request.getData() instanceof SmsLoginRequest) {
SmsLoginRequest smsRequest = (SmsLoginRequest) request.getData();
if (smsRequest.getPhone() == null || smsRequest.getPhone().isEmpty()) {
return Mono.error(new ApplicationException(CLIENT_SMS_CODE_INVALID));
}
if (smsRequest.getCode() == null || smsRequest.getCode().isEmpty()) {
return Mono.error(new ApplicationException(CLIENT_SMS_CODE_INVALID));
}
return Mono.just(request);
}
return Mono.error(new ApplicationException(CLIENT_SMS_CODE_INVALID));
}
@Override
protected Mono<LoginResult> doLogin(LoginRequest request) {
SmsLoginRequest smsRequest = (SmsLoginRequest) request.getData();
return smsVerificationCodeRepository
.findLatestByPhoneAndType(smsRequest.getPhone(), SmsCodeType.LOGIN.getCode())
.switchIfEmpty(Mono.error(new ApplicationException(CLIENT_SMS_CODE_INVALID)))
.flatMap(verificationCode -> {
if (verificationCode.isExpired()) {
return Mono.error(new ApplicationException(CLIENT_SMS_CODE_EXPIRED));
}
if (verificationCode.isVerified()) {
return Mono.error(new ApplicationException(CLIENT_SMS_CODE_USED));
}
if (verificationCode.getVerifyCount() != null
&& verificationCode.getVerifyCount() >= MAX_VERIFY_COUNT) {
return Mono.error(new ApplicationException(CLIENT_SMS_CODE_EXCEEDED));
}
if (!smsRequest.getCode().equals(verificationCode.getCode())) {
verificationCode.incrementVerifyCount();
return smsVerificationCodeRepository.save(verificationCode)
.then(Mono.error(new ApplicationException(CLIENT_SMS_CODE_INVALID)));
}
verificationCode.markAsVerified();
return smsVerificationCodeRepository.markAsVerified(verificationCode.getId())
.thenReturn(verificationCode);
})
.flatMap(verificationCode -> clientUserRepository.findByPhone(
io.destiny.common.primitive.PhoneNumber.of(smsRequest.getPhone())))
.switchIfEmpty(Mono.defer(() -> createNewUser(smsRequest.getPhone())))
.map(user -> buildLoginResult(user, "sms"));
}
private Mono<ClientUser> createNewUser(String phone) {
ClientUser newUser = new ClientUser();
newUser.generateId();
newUser.setPhone(io.destiny.common.primitive.PhoneNumber.of(phone));
newUser.setPhoneVerified(true);
newUser.setUsername(phone);
return clientUserRepository.save(newUser);
}
private LoginResult buildLoginResult(ClientUser user, String platform) {
LoginResult result = new LoginResult();
result.setUserId(user.getId());
result.setUsername(user.getUsername());
result.setPhone(user.getPhone() != null ? user.getPhone().getValue() : null);
result.setPlatform(platform);
return result;
}
}
@@ -0,0 +1,101 @@
package io.destiny.client.core.strategy;
import io.destiny.client.core.domain.ClientLoginLog;
import io.destiny.client.core.domain.ClientUser;
import io.destiny.client.core.repository.IClientLoginLogRepository;
import io.destiny.client.core.repository.IClientUserRepository;
import io.destiny.client.core.service.ClientUserAuthService;
import io.destiny.client.dto.LoginRequest;
import io.destiny.client.dto.LoginResult;
import io.destiny.client.security.JwtTokenProvider;
import io.destiny.common.exception.ApplicationException;
import io.destiny.common.primitive.IpAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.Map;
import static io.destiny.client.core.exception.ClientUserAuthExceptionCode.CLIENT_INVALID_USERNAME_OR_PASSWORD;
@Component
public class UsernamePasswordLoginStrategy extends AbstractLoginStrategy {
private static final Logger logger = LoggerFactory.getLogger(UsernamePasswordLoginStrategy.class);
private final IClientUserRepository clientUserRepository;
private final IClientLoginLogRepository clientLoginLogRepository;
private final PasswordEncoder passwordEncoder;
public UsernamePasswordLoginStrategy(ClientUserAuthService clientUserAuthService,
JwtTokenProvider jwtTokenProvider,
IClientUserRepository clientUserRepository,
IClientLoginLogRepository clientLoginLogRepository,
PasswordEncoder passwordEncoder) {
super(clientUserAuthService, jwtTokenProvider);
this.clientUserRepository = clientUserRepository;
this.clientLoginLogRepository = clientLoginLogRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
public String getLoginType() {
return "password";
}
@Override
protected Mono<LoginRequest> validateRequest(LoginRequest request) {
if (request.getData() instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) request.getData();
String username = (String) data.get("username");
String password = (String) data.get("password");
if (username == null || username.isEmpty()) {
return Mono.error(new ApplicationException(CLIENT_INVALID_USERNAME_OR_PASSWORD));
}
if (password == null || password.isEmpty()) {
return Mono.error(new ApplicationException(CLIENT_INVALID_USERNAME_OR_PASSWORD));
}
return Mono.just(request);
}
return Mono.error(new ApplicationException(CLIENT_INVALID_USERNAME_OR_PASSWORD));
}
@Override
protected Mono<LoginResult> doLogin(LoginRequest request) {
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) request.getData();
String username = (String) data.get("username");
String password = (String) data.get("password");
logger.info("Executing username password login - Username: {}, IP: {}",
username, request.getIpAddress());
return clientUserRepository.findByUsername(username)
.switchIfEmpty(Mono.error(new ApplicationException(CLIENT_INVALID_USERNAME_OR_PASSWORD)))
.filter(user -> passwordEncoder.matches(password, user.getPassword()))
.switchIfEmpty(Mono.error(new ApplicationException(CLIENT_INVALID_USERNAME_OR_PASSWORD)))
.flatMap(user -> recordLoginLog(user, request.getIpAddress())
.thenReturn(user))
.map(user -> {
logger.info("Username password login successful - UserId: {}, Username: {}",
user.getId(), username);
LoginResult result = new LoginResult();
result.setUserId(user.getId());
result.setUsername(user.getUsername());
result.setPlatform(getLoginType());
return result;
});
}
private Mono<Void> recordLoginLog(ClientUser user, String ipAddress) {
ClientLoginLog loginLog = new ClientLoginLog(user.getId(), IpAddress.of(ipAddress));
return clientLoginLogRepository.save(loginLog)
.doOnSuccess(log -> logger.debug("Login log recorded - UserId: {}, IP: {}",
user.getId(), ipAddress))
.then();
}
}
@@ -0,0 +1,115 @@
package io.destiny.client.core.strategy;
import io.destiny.client.client.WechatMiniProgramClient;
import io.destiny.client.core.domain.ClientUser;
import io.destiny.client.core.domain.UserBinding;
import io.destiny.client.core.repository.IClientUserRepository;
import io.destiny.client.core.repository.IUserBindingRepository;
import io.destiny.client.core.service.ClientUserAuthService;
import io.destiny.client.dto.LoginRequest;
import io.destiny.client.dto.LoginResult;
import io.destiny.client.dto.WechatLoginRequest;
import io.destiny.client.security.JwtTokenProvider;
import io.destiny.common.exception.ApplicationException;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import static io.destiny.client.core.exception.ClientUserAuthExceptionCode.*;
@Component
public class WechatLoginStrategy extends AbstractLoginStrategy {
private static final String LOGIN_TYPE = "wechat";
private final WechatMiniProgramClient wechatClient;
private final IClientUserRepository clientUserRepository;
private final IUserBindingRepository userBindingRepository;
public WechatLoginStrategy(ClientUserAuthService clientUserAuthService,
JwtTokenProvider jwtTokenProvider,
WechatMiniProgramClient wechatClient,
IClientUserRepository clientUserRepository,
IUserBindingRepository userBindingRepository) {
super(clientUserAuthService, jwtTokenProvider);
this.wechatClient = wechatClient;
this.clientUserRepository = clientUserRepository;
this.userBindingRepository = userBindingRepository;
}
@Override
public String getLoginType() {
return LOGIN_TYPE;
}
@Override
protected Mono<LoginRequest> validateRequest(LoginRequest request) {
if (request.getData() instanceof WechatLoginRequest) {
WechatLoginRequest wechatRequest = (WechatLoginRequest) request.getData();
if (wechatRequest.getCode() == null || wechatRequest.getCode().isEmpty()) {
return Mono.error(new ApplicationException(CLIENT_INVALID_WECHAT_CODE));
}
return Mono.just(request);
}
return Mono.error(new ApplicationException(CLIENT_INVALID_WECHAT_CODE));
}
@Override
protected Mono<LoginResult> doLogin(LoginRequest request) {
WechatLoginRequest wechatRequest = (WechatLoginRequest) request.getData();
return wechatClient.auth(wechatRequest.getCode())
.flatMap(authResponse -> {
String openId = authResponse.getOpenId();
String unionId = authResponse.getUnionId();
return clientUserRepository.findByWechatOpenId(openId)
.switchIfEmpty(Mono.defer(() -> {
if (unionId != null && !unionId.isEmpty()) {
return clientUserRepository.findByWechatUnionId(unionId);
}
return Mono.empty();
}))
.switchIfEmpty(Mono.defer(() -> {
if (unionId != null && !unionId.isEmpty()) {
return userBindingRepository.findByPlatformAndUnionId("wechat", unionId)
.flatMap(binding -> clientUserRepository.findById(binding.getUserId()));
}
return Mono.empty();
}))
.switchIfEmpty(Mono.defer(() -> createNewUser(openId, unionId)))
.map(user -> buildLoginResult(user, "wechat"));
});
}
private Mono<ClientUser> createNewUser(String openId, String unionId) {
ClientUser newUser = new ClientUser();
newUser.generateId();
newUser.setWechatOpenId(openId);
if (unionId != null && !unionId.isEmpty()) {
newUser.setWechatUnionId(unionId);
}
newUser.setPhoneVerified(false);
return clientUserRepository.save(newUser)
.flatMap(user -> {
UserBinding binding = new UserBinding();
binding.generateId();
binding.setUserId(user.getId());
binding.setPlatform("wechat");
binding.setPlatformOpenId(openId);
if (unionId != null && !unionId.isEmpty()) {
binding.setPlatformUnionId(unionId);
}
return userBindingRepository.save(binding).thenReturn(user);
});
}
private LoginResult buildLoginResult(ClientUser user, String platform) {
LoginResult result = new LoginResult();
result.setUserId(user.getId());
result.setUsername(user.getUsername());
result.setPhone(user.getPhone() != null ? user.getPhone().getValue() : null);
result.setPlatform(platform);
return result;
}
}
@@ -0,0 +1,24 @@
package io.destiny.client.dto;
public class AccountBindRequest {
private String platform;
private String code;
public String getPlatform() {
return platform;
}
public void setPlatform(String platform) {
this.platform = platform;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}
@@ -0,0 +1,41 @@
package io.destiny.client.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.destiny.client.core.domain.ClientUser;
public class ClientLoginResponse {
@JsonProperty("token")
private String token;
@JsonProperty("user")
private ClientUserResponse user;
public ClientLoginResponse() {
}
public ClientLoginResponse(String token, ClientUser user) {
this.token = token;
this.user = ClientUserResponse.from(user);
}
public static ClientLoginResponse from(String token, ClientUser user) {
return new ClientLoginResponse(token, user);
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public ClientUserResponse getUser() {
return user;
}
public void setUser(ClientUserResponse user) {
this.user = user;
}
}
@@ -0,0 +1,45 @@
package io.destiny.client.dto;
import io.destiny.client.core.domain.query.ClientUserLogin;
import io.destiny.common.primitive.IpAddress;
public class ClientUserLoginRequest {
private String username;
private String password;
private String ipAddress;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public ClientUserLogin toDomain() {
ClientUserLogin login = new ClientUserLogin();
login.setUsername(this.username);
login.setPassword(this.password);
login.setIpAddress(IpAddress.of(this.ipAddress));
return login;
}
}
@@ -0,0 +1,66 @@
package io.destiny.client.dto;
import io.destiny.client.core.domain.ClientUser;
public class ClientUserRegisterRequest {
private String username;
private String password;
private String email;
private String phone;
private String gender;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public ClientUser toDomain() {
ClientUser user = new ClientUser();
user.setUsername(this.username);
user.setPassword(this.password);
user.setEmail(io.destiny.common.primitive.EmailAddress.of(this.email));
user.setPhone(io.destiny.common.primitive.PhoneNumber.of(this.phone));
user.setGender(this.gender);
return user;
}
}
@@ -0,0 +1,82 @@
package io.destiny.client.dto;
import io.destiny.client.core.domain.ClientUser;
public class ClientUserResponse {
private Long id;
private String username;
private String email;
private String phone;
private String gender;
private String token;
public static ClientUserResponse from(ClientUser user) {
ClientUserResponse response = new ClientUserResponse();
response.setId(user.getId());
response.setUsername(user.getUsername());
response.setEmail(user.getEmail() != null ? user.getEmail().getValue() : null);
response.setPhone(user.getPhone() != null ? user.getPhone().getValue() : null);
response.setGender(user.getGender());
return response;
}
public static ClientUserResponse from(ClientUser user, String token) {
ClientUserResponse response = from(user);
response.setToken(token);
return response;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
@@ -0,0 +1,34 @@
package io.destiny.client.dto;
public class DouyinLoginRequest {
private String code;
private String platform;
private String ipAddress;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getPlatform() {
return platform;
}
public void setPlatform(String platform) {
this.platform = platform;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
}
@@ -0,0 +1,34 @@
package io.destiny.client.dto;
public class LoginRequest {
private String loginType;
private String ipAddress;
private Object data;
public String getLoginType() {
return loginType;
}
public void setLoginType(String loginType) {
this.loginType = loginType;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
@@ -0,0 +1,54 @@
package io.destiny.client.dto;
public class LoginResult {
private String token;
private Long userId;
private String username;
private String phone;
private String platform;
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getPlatform() {
return platform;
}
public void setPlatform(String platform) {
this.platform = platform;
}
}
@@ -0,0 +1,34 @@
package io.destiny.client.dto;
public class PasswordLoginRequest {
private String username;
private String password;
private String ipAddress;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
}
@@ -0,0 +1,24 @@
package io.destiny.client.dto;
public class SmsCodeSendRequest {
private String phone;
private String type;
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
@@ -0,0 +1,34 @@
package io.destiny.client.dto;
public class SmsLoginRequest {
private String phone;
private String code;
private String ipAddress;
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
}
@@ -0,0 +1,34 @@
package io.destiny.client.dto;
public class WechatAuthResponse {
private String openId;
private String unionId;
private String sessionKey;
public String getOpenId() {
return openId;
}
public void setOpenId(String openId) {
this.openId = openId;
}
public String getUnionId() {
return unionId;
}
public void setUnionId(String unionId) {
this.unionId = unionId;
}
public String getSessionKey() {
return sessionKey;
}
public void setSessionKey(String sessionKey) {
this.sessionKey = sessionKey;
}
}
@@ -0,0 +1,34 @@
package io.destiny.client.dto;
public class WechatLoginRequest {
private String code;
private String platform;
private String ipAddress;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getPlatform() {
return platform;
}
public void setPlatform(String platform) {
this.platform = platform;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
}
@@ -0,0 +1,108 @@
package io.destiny.client.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component("clientJwtTokenProvider")
public class JwtTokenProvider {
private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);
@Value("${jwt.secret:this-is-a-secure-jwt-secret-key-that-must-be-at-least-64-characters-long-for-hs512-algorithm}")
private String secret;
@Value("${jwt.expiration:86400000}")
private Long expiration;
private SecretKey getSigningKey() {
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
public String generateToken(Long userId, String username) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
return createToken(claims, userId.toString());
}
private String createToken(Map<String, Object> claims, String subject) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(getSigningKey())
.compact();
}
public Long getUserIdFromToken(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
return claims.get("userId", Long.class);
} catch (Exception e) {
logger.error("Failed to get user id from token", e);
return null;
}
}
public String getUsernameFromToken(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
return claims.get("username", String.class);
} catch (Exception e) {
logger.error("Failed to get username from token", e);
return null;
}
}
public Boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token);
return true;
} catch (Exception e) {
logger.error("Invalid JWT token", e);
return false;
}
}
public Boolean isTokenExpired(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
logger.error("Failed to check token expiration", e);
return true;
}
}
}
@@ -0,0 +1,231 @@
package io.destiny.client.client;
import io.destiny.client.config.DouyinMiniProgramConfig;
import io.destiny.client.core.exception.ClientUserAuthExceptionCode;
import io.destiny.client.dto.WechatAuthResponse;
import io.destiny.common.exception.ApplicationException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.function.Function;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@SuppressWarnings({"unchecked", "rawtypes"})
class DouyinMiniProgramClientTest {
@Mock
private WebClient webClient;
@Mock
private WebClient.RequestHeadersUriSpec requestHeadersUriSpec;
@Mock
private WebClient.RequestHeadersSpec requestHeadersSpec;
@Mock
private WebClient.ResponseSpec responseSpec;
@Mock
private DouyinMiniProgramConfig config;
private DouyinMiniProgramClient douyinMiniProgramClient;
private static final String TEST_CODE = "test_code_123";
private static final String TEST_APP_ID = "test_app_id";
private static final String TEST_APP_SECRET = "test_app_secret";
private static final String TEST_OPEN_ID = "test_open_id";
private static final String TEST_SESSION_KEY = "test_session_key";
private static final String TEST_UNION_ID = "test_union_id";
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
when(config.getAppId()).thenReturn(TEST_APP_ID);
when(config.getAppSecret()).thenReturn(TEST_APP_SECRET);
douyinMiniProgramClient = new DouyinMiniProgramClient(config, webClient);
}
@Test
void testAuth_Success() {
String successResponse = String.format("{\"err_no\":0,\"err_tips\":\"success\",\"data\":{\"openid\":\"%s\",\"session_key\":\"%s\",\"unionid\":\"%s\"}}",
TEST_OPEN_ID, TEST_SESSION_KEY, TEST_UNION_ID);
when(webClient.get()).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.uri(anyString(), any(Function.class))).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.accept(MediaType.APPLICATION_JSON)).thenReturn(requestHeadersSpec);
when(requestHeadersSpec.retrieve()).thenReturn(responseSpec);
when(responseSpec.bodyToMono(String.class)).thenReturn(Mono.just(successResponse));
Mono<WechatAuthResponse> result = douyinMiniProgramClient.auth(TEST_CODE);
StepVerifier.create(result)
.expectNextMatches(response ->
TEST_OPEN_ID.equals(response.getOpenId()) &&
TEST_SESSION_KEY.equals(response.getSessionKey()) &&
TEST_UNION_ID.equals(response.getUnionId()))
.verifyComplete();
verify(webClient, times(1)).get();
verify(requestHeadersUriSpec, times(1)).uri(anyString(), any(Function.class));
verify(requestHeadersUriSpec, times(1)).accept(MediaType.APPLICATION_JSON);
verify(requestHeadersSpec, times(1)).retrieve();
verify(responseSpec, times(1)).bodyToMono(String.class);
}
@Test
void testAuth_WebClientResponseException() {
WebClientResponseException exception = WebClientResponseException.create(
400, "Bad Request", null, "{\"errcode\":40029,\"errmsg\":\"invalid code\"}".getBytes(), null);
when(webClient.get()).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.uri(anyString(), any(Function.class))).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.accept(MediaType.APPLICATION_JSON)).thenReturn(requestHeadersSpec);
when(requestHeadersSpec.retrieve()).thenReturn(responseSpec);
when(responseSpec.bodyToMono(String.class)).thenReturn(Mono.error(exception));
Mono<WechatAuthResponse> result = douyinMiniProgramClient.auth(TEST_CODE);
StepVerifier.create(result)
.expectErrorMatches(ex -> ex instanceof ApplicationException &&
((ApplicationException) ex).getExceptionCode().equals(ClientUserAuthExceptionCode.CLIENT_DOUYIN_AUTH_FAILED))
.verify();
verify(webClient, times(1)).get();
verify(requestHeadersUriSpec, times(1)).uri(anyString(), any(Function.class));
verify(requestHeadersUriSpec, times(1)).accept(MediaType.APPLICATION_JSON);
verify(requestHeadersSpec, times(1)).retrieve();
verify(responseSpec, times(1)).bodyToMono(String.class);
}
@Test
void testAuth_GeneralException() {
RuntimeException exception = new RuntimeException("Network error");
when(webClient.get()).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.uri(anyString(), any(Function.class))).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.accept(MediaType.APPLICATION_JSON)).thenReturn(requestHeadersSpec);
when(requestHeadersSpec.retrieve()).thenReturn(responseSpec);
when(responseSpec.bodyToMono(String.class)).thenReturn(Mono.error(exception));
Mono<WechatAuthResponse> result = douyinMiniProgramClient.auth(TEST_CODE);
StepVerifier.create(result)
.expectErrorMatches(ex -> ex instanceof ApplicationException &&
((ApplicationException) ex).getExceptionCode().equals(ClientUserAuthExceptionCode.CLIENT_DOUYIN_AUTH_FAILED))
.verify();
verify(webClient, times(1)).get();
verify(requestHeadersUriSpec, times(1)).uri(anyString(), any(Function.class));
verify(requestHeadersUriSpec, times(1)).accept(MediaType.APPLICATION_JSON);
verify(requestHeadersSpec, times(1)).retrieve();
verify(responseSpec, times(1)).bodyToMono(String.class);
}
@Test
void testAuth_InvalidResponseFormat() {
String invalidResponse = "invalid json";
when(webClient.get()).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.uri(anyString(), any(Function.class))).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.accept(MediaType.APPLICATION_JSON)).thenReturn(requestHeadersSpec);
when(requestHeadersSpec.retrieve()).thenReturn(responseSpec);
when(responseSpec.bodyToMono(String.class)).thenReturn(Mono.just(invalidResponse));
Mono<WechatAuthResponse> result = douyinMiniProgramClient.auth(TEST_CODE);
StepVerifier.create(result)
.expectErrorMatches(ex -> ex instanceof ApplicationException &&
((ApplicationException) ex).getExceptionCode().equals(ClientUserAuthExceptionCode.CLIENT_DOUYIN_AUTH_FAILED))
.verify();
verify(webClient, times(1)).get();
verify(requestHeadersUriSpec, times(1)).uri(anyString(), any(Function.class));
verify(requestHeadersUriSpec, times(1)).accept(MediaType.APPLICATION_JSON);
verify(requestHeadersSpec, times(1)).retrieve();
verify(responseSpec, times(1)).bodyToMono(String.class);
}
@Test
void testAuth_ResponseWithoutUnionId() {
String responseWithoutUnionId = String.format("{\"err_no\":0,\"err_tips\":\"success\",\"data\":{\"openid\":\"%s\",\"session_key\":\"%s\"}}",
TEST_OPEN_ID, TEST_SESSION_KEY);
when(webClient.get()).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.uri(anyString(), any(Function.class))).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.accept(MediaType.APPLICATION_JSON)).thenReturn(requestHeadersSpec);
when(requestHeadersSpec.retrieve()).thenReturn(responseSpec);
when(responseSpec.bodyToMono(String.class)).thenReturn(Mono.just(responseWithoutUnionId));
Mono<WechatAuthResponse> result = douyinMiniProgramClient.auth(TEST_CODE);
StepVerifier.create(result)
.expectNextMatches(response ->
TEST_OPEN_ID.equals(response.getOpenId()) &&
TEST_SESSION_KEY.equals(response.getSessionKey()) &&
response.getUnionId() == null)
.verifyComplete();
verify(webClient, times(1)).get();
verify(requestHeadersUriSpec, times(1)).uri(anyString(), any(Function.class));
verify(requestHeadersUriSpec, times(1)).accept(MediaType.APPLICATION_JSON);
verify(requestHeadersSpec, times(1)).retrieve();
verify(responseSpec, times(1)).bodyToMono(String.class);
}
@Test
void testAuth_ApiError() {
String errorResponse = "{\"err_no\":40029,\"err_tips\":\"invalid code\"}";
when(webClient.get()).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.uri(anyString(), any(Function.class))).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.accept(MediaType.APPLICATION_JSON)).thenReturn(requestHeadersSpec);
when(requestHeadersSpec.retrieve()).thenReturn(responseSpec);
when(responseSpec.bodyToMono(String.class)).thenReturn(Mono.just(errorResponse));
Mono<WechatAuthResponse> result = douyinMiniProgramClient.auth(TEST_CODE);
StepVerifier.create(result)
.expectErrorMatches(ex -> ex instanceof ApplicationException &&
((ApplicationException) ex).getExceptionCode().equals(ClientUserAuthExceptionCode.CLIENT_DOUYIN_AUTH_FAILED))
.verify();
verify(webClient, times(1)).get();
verify(requestHeadersUriSpec, times(1)).uri(anyString(), any(Function.class));
verify(requestHeadersUriSpec, times(1)).accept(MediaType.APPLICATION_JSON);
verify(requestHeadersSpec, times(1)).retrieve();
verify(responseSpec, times(1)).bodyToMono(String.class);
}
@Test
void testAuth_ResponseWithoutData() {
String responseWithoutData = "{\"err_no\":0,\"err_tips\":\"success\"}";
when(webClient.get()).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.uri(anyString(), any(Function.class))).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.accept(MediaType.APPLICATION_JSON)).thenReturn(requestHeadersSpec);
when(requestHeadersSpec.retrieve()).thenReturn(responseSpec);
when(responseSpec.bodyToMono(String.class)).thenReturn(Mono.just(responseWithoutData));
Mono<WechatAuthResponse> result = douyinMiniProgramClient.auth(TEST_CODE);
StepVerifier.create(result)
.expectErrorMatches(ex -> ex instanceof ApplicationException &&
((ApplicationException) ex).getExceptionCode().equals(ClientUserAuthExceptionCode.CLIENT_DOUYIN_AUTH_FAILED))
.verify();
verify(webClient, times(1)).get();
verify(requestHeadersUriSpec, times(1)).uri(anyString(), any(Function.class));
verify(requestHeadersUriSpec, times(1)).accept(MediaType.APPLICATION_JSON);
verify(requestHeadersSpec, times(1)).retrieve();
verify(responseSpec, times(1)).bodyToMono(String.class);
}
}
@@ -0,0 +1,183 @@
package io.destiny.client.client;
import io.destiny.client.config.WechatMiniProgramConfig;
import io.destiny.client.core.exception.ClientUserAuthExceptionCode;
import io.destiny.client.dto.WechatAuthResponse;
import io.destiny.common.exception.ApplicationException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.function.Function;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@SuppressWarnings({"unchecked", "rawtypes"})
class WechatMiniProgramClientTest {
@Mock
private WebClient webClient;
@Mock
private WebClient.RequestHeadersUriSpec requestHeadersUriSpec;
@Mock
private WebClient.RequestHeadersSpec requestHeadersSpec;
@Mock
private WebClient.ResponseSpec responseSpec;
@Mock
private WechatMiniProgramConfig config;
private WechatMiniProgramClient wechatMiniProgramClient;
private static final String TEST_CODE = "test_code_123";
private static final String TEST_APP_ID = "test_app_id";
private static final String TEST_APP_SECRET = "test_app_secret";
private static final String TEST_OPEN_ID = "test_open_id";
private static final String TEST_SESSION_KEY = "test_session_key";
private static final String TEST_UNION_ID = "test_union_id";
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
when(config.getAppId()).thenReturn(TEST_APP_ID);
when(config.getAppSecret()).thenReturn(TEST_APP_SECRET);
wechatMiniProgramClient = new WechatMiniProgramClient(config, webClient);
}
@Test
void testAuth_Success() {
String successResponse = String.format("{\"openid\":\"%s\",\"session_key\":\"%s\",\"unionid\":\"%s\"}",
TEST_OPEN_ID, TEST_SESSION_KEY, TEST_UNION_ID);
when(webClient.get()).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.uri(anyString(), any(Function.class))).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.accept(MediaType.APPLICATION_JSON)).thenReturn(requestHeadersSpec);
when(requestHeadersSpec.retrieve()).thenReturn(responseSpec);
when(responseSpec.bodyToMono(String.class)).thenReturn(Mono.just(successResponse));
Mono<WechatAuthResponse> result = wechatMiniProgramClient.auth(TEST_CODE);
StepVerifier.create(result)
.expectNextMatches(response ->
TEST_OPEN_ID.equals(response.getOpenId()) &&
TEST_SESSION_KEY.equals(response.getSessionKey()) &&
TEST_UNION_ID.equals(response.getUnionId()))
.verifyComplete();
verify(webClient, times(1)).get();
verify(requestHeadersUriSpec, times(1)).uri(anyString(), any(Function.class));
verify(requestHeadersUriSpec, times(1)).accept(MediaType.APPLICATION_JSON);
verify(requestHeadersSpec, times(1)).retrieve();
verify(responseSpec, times(1)).bodyToMono(String.class);
}
@Test
void testAuth_WebClientResponseException() {
WebClientResponseException exception = WebClientResponseException.create(
400, "Bad Request", null, "{\"errcode\":40029,\"errmsg\":\"invalid code\"}".getBytes(), null);
when(webClient.get()).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.uri(anyString(), any(Function.class))).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.accept(MediaType.APPLICATION_JSON)).thenReturn(requestHeadersSpec);
when(requestHeadersSpec.retrieve()).thenReturn(responseSpec);
when(responseSpec.bodyToMono(String.class)).thenReturn(Mono.error(exception));
Mono<WechatAuthResponse> result = wechatMiniProgramClient.auth(TEST_CODE);
StepVerifier.create(result)
.expectErrorMatches(ex -> ex instanceof ApplicationException &&
((ApplicationException) ex).getExceptionCode().equals(ClientUserAuthExceptionCode.CLIENT_WECHAT_AUTH_FAILED))
.verify();
verify(webClient, times(1)).get();
verify(requestHeadersUriSpec, times(1)).uri(anyString(), any(Function.class));
verify(requestHeadersUriSpec, times(1)).accept(MediaType.APPLICATION_JSON);
verify(requestHeadersSpec, times(1)).retrieve();
verify(responseSpec, times(1)).bodyToMono(String.class);
}
@Test
void testAuth_GeneralException() {
RuntimeException exception = new RuntimeException("Network error");
when(webClient.get()).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.uri(anyString(), any(Function.class))).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.accept(MediaType.APPLICATION_JSON)).thenReturn(requestHeadersSpec);
when(requestHeadersSpec.retrieve()).thenReturn(responseSpec);
when(responseSpec.bodyToMono(String.class)).thenReturn(Mono.error(exception));
Mono<WechatAuthResponse> result = wechatMiniProgramClient.auth(TEST_CODE);
StepVerifier.create(result)
.expectErrorMatches(ex -> ex instanceof ApplicationException &&
((ApplicationException) ex).getExceptionCode().equals(ClientUserAuthExceptionCode.CLIENT_WECHAT_AUTH_FAILED))
.verify();
verify(webClient, times(1)).get();
verify(requestHeadersUriSpec, times(1)).uri(anyString(), any(Function.class));
verify(requestHeadersUriSpec, times(1)).accept(MediaType.APPLICATION_JSON);
verify(requestHeadersSpec, times(1)).retrieve();
verify(responseSpec, times(1)).bodyToMono(String.class);
}
@Test
void testAuth_InvalidResponseFormat() {
String invalidResponse = "invalid json";
when(webClient.get()).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.uri(anyString(), any(Function.class))).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.accept(MediaType.APPLICATION_JSON)).thenReturn(requestHeadersSpec);
when(requestHeadersSpec.retrieve()).thenReturn(responseSpec);
when(responseSpec.bodyToMono(String.class)).thenReturn(Mono.just(invalidResponse));
Mono<WechatAuthResponse> result = wechatMiniProgramClient.auth(TEST_CODE);
StepVerifier.create(result)
.expectErrorMatches(ex -> ex instanceof ApplicationException &&
((ApplicationException) ex).getExceptionCode().equals(ClientUserAuthExceptionCode.CLIENT_WECHAT_AUTH_FAILED))
.verify();
verify(webClient, times(1)).get();
verify(requestHeadersUriSpec, times(1)).uri(anyString(), any(Function.class));
verify(requestHeadersUriSpec, times(1)).accept(MediaType.APPLICATION_JSON);
verify(requestHeadersSpec, times(1)).retrieve();
verify(responseSpec, times(1)).bodyToMono(String.class);
}
@Test
void testAuth_ResponseWithoutUnionId() {
String responseWithoutUnionId = String.format("{\"openid\":\"%s\",\"session_key\":\"%s\"}",
TEST_OPEN_ID, TEST_SESSION_KEY);
when(webClient.get()).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.uri(anyString(), any(Function.class))).thenReturn(requestHeadersUriSpec);
when(requestHeadersUriSpec.accept(MediaType.APPLICATION_JSON)).thenReturn(requestHeadersSpec);
when(requestHeadersSpec.retrieve()).thenReturn(responseSpec);
when(responseSpec.bodyToMono(String.class)).thenReturn(Mono.just(responseWithoutUnionId));
Mono<WechatAuthResponse> result = wechatMiniProgramClient.auth(TEST_CODE);
StepVerifier.create(result)
.expectNextMatches(response ->
TEST_OPEN_ID.equals(response.getOpenId()) &&
TEST_SESSION_KEY.equals(response.getSessionKey()) &&
response.getUnionId() == null)
.verifyComplete();
verify(webClient, times(1)).get();
verify(requestHeadersUriSpec, times(1)).uri(anyString(), any(Function.class));
verify(requestHeadersUriSpec, times(1)).accept(MediaType.APPLICATION_JSON);
verify(requestHeadersSpec, times(1)).retrieve();
verify(responseSpec, times(1)).bodyToMono(String.class);
}
}
@@ -0,0 +1,55 @@
package io.destiny.client.config;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class AliyunSmsConfigTest {
private AliyunSmsConfig aliyunSmsConfig;
@BeforeEach
void setUp() {
aliyunSmsConfig = new AliyunSmsConfig();
}
@Test
void testGettersAndSetters_Success() {
aliyunSmsConfig.setAccessKeyId("testAccessKeyId");
aliyunSmsConfig.setAccessKeySecret("testAccessKeySecret");
aliyunSmsConfig.setEndpoint("testEndpoint");
aliyunSmsConfig.setSignName("testSignName");
aliyunSmsConfig.setTemplateCode("testTemplateCode");
assertEquals("testAccessKeyId", aliyunSmsConfig.getAccessKeyId());
assertEquals("testAccessKeySecret", aliyunSmsConfig.getAccessKeySecret());
assertEquals("testEndpoint", aliyunSmsConfig.getEndpoint());
assertEquals("testSignName", aliyunSmsConfig.getSignName());
assertEquals("testTemplateCode", aliyunSmsConfig.getTemplateCode());
}
@Test
void testDefaultValues_Success() {
assertEquals(10, aliyunSmsConfig.getExpireMinutes());
assertEquals(60, aliyunSmsConfig.getSendIntervalSeconds());
assertEquals(10, aliyunSmsConfig.getMaxSendCountPerDay());
assertEquals(5, aliyunSmsConfig.getMaxVerifyAttempts());
assertEquals(10, aliyunSmsConfig.getLockMinutes());
}
@Test
void testCustomValues_Success() {
aliyunSmsConfig.setExpireMinutes(5);
aliyunSmsConfig.setSendIntervalSeconds(30);
aliyunSmsConfig.setMaxSendCountPerDay(20);
aliyunSmsConfig.setMaxVerifyAttempts(3);
aliyunSmsConfig.setLockMinutes(15);
assertEquals(5, aliyunSmsConfig.getExpireMinutes());
assertEquals(30, aliyunSmsConfig.getSendIntervalSeconds());
assertEquals(20, aliyunSmsConfig.getMaxSendCountPerDay());
assertEquals(3, aliyunSmsConfig.getMaxVerifyAttempts());
assertEquals(15, aliyunSmsConfig.getLockMinutes());
}
}
@@ -0,0 +1,31 @@
package io.destiny.client.config;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class DouyinMiniProgramConfigTest {
private DouyinMiniProgramConfig douyinMiniProgramConfig;
@BeforeEach
void setUp() {
douyinMiniProgramConfig = new DouyinMiniProgramConfig();
}
@Test
void testGettersAndSetters_Success() {
douyinMiniProgramConfig.setAppId("testAppId");
douyinMiniProgramConfig.setAppSecret("testAppSecret");
assertEquals("testAppId", douyinMiniProgramConfig.getAppId());
assertEquals("testAppSecret", douyinMiniProgramConfig.getAppSecret());
}
@Test
void testNullValues_Success() {
assertNull(douyinMiniProgramConfig.getAppId());
assertNull(douyinMiniProgramConfig.getAppSecret());
}
}
@@ -0,0 +1,31 @@
package io.destiny.client.config;
import com.aliyun.dysmsapi20170525.Client;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class SmsClientConfigTest {
private SmsClientConfig smsClientConfig;
@Mock
private AliyunSmsConfig smsConfig;
@BeforeEach
void setUp() {
smsClientConfig = new SmsClientConfig();
}
@Test
void testSmsClientBean_Success() throws Exception {
Client client = smsClientConfig.smsClient(smsConfig);
assertNotNull(client);
}
}
@@ -0,0 +1,31 @@
package io.destiny.client.config;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class WechatMiniProgramConfigTest {
private WechatMiniProgramConfig wechatMiniProgramConfig;
@BeforeEach
void setUp() {
wechatMiniProgramConfig = new WechatMiniProgramConfig();
}
@Test
void testGettersAndSetters_Success() {
wechatMiniProgramConfig.setAppId("testAppId");
wechatMiniProgramConfig.setAppSecret("testAppSecret");
assertEquals("testAppId", wechatMiniProgramConfig.getAppId());
assertEquals("testAppSecret", wechatMiniProgramConfig.getAppSecret());
}
@Test
void testNullValues_Success() {
assertNull(wechatMiniProgramConfig.getAppId());
assertNull(wechatMiniProgramConfig.getAppSecret());
}
}
@@ -0,0 +1,171 @@
package io.destiny.client.core.domain;
import io.destiny.common.primitive.IpAddress;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.*;
class ClientLoginLogTest {
private static final Long TEST_ID = 12345L;
private static final Long TEST_CLIENT_USER_ID = 67890L;
private static final IpAddress TEST_IP_ADDRESS = IpAddress.of("192.168.1.1");
@Test
void testConstructor_DefaultValues() {
ClientLoginLog log = new ClientLoginLog();
assertNull(log.getId());
assertNull(log.getClientUserId());
assertNull(log.getIpAddress());
assertNull(log.getLoginTime());
assertNull(log.getLogoutTime());
assertNull(log.getCreatedAt());
assertNull(log.getUpdatedAt());
assertNull(log.getDeletedAt());
}
@Test
void testConstructor_WithParameters() {
ClientLoginLog log = new ClientLoginLog(TEST_CLIENT_USER_ID, TEST_IP_ADDRESS);
assertNotNull(log.getId());
assertTrue(log.getId() > 0);
assertEquals(TEST_CLIENT_USER_ID, log.getClientUserId());
assertEquals(TEST_IP_ADDRESS, log.getIpAddress());
assertNotNull(log.getLoginTime());
assertTrue(log.getLoginTime().isBefore(LocalDateTime.now().plusSeconds(1)));
assertTrue(log.getLoginTime().isAfter(LocalDateTime.now().minusSeconds(1)));
assertNull(log.getLogoutTime());
assertNull(log.getCreatedAt());
assertNull(log.getUpdatedAt());
assertNull(log.getDeletedAt());
}
@Test
void testGettersAndSetters() {
ClientLoginLog log = new ClientLoginLog();
log.setId(TEST_ID);
log.setClientUserId(TEST_CLIENT_USER_ID);
log.setIpAddress(TEST_IP_ADDRESS);
LocalDateTime testLoginTime = LocalDateTime.now().minusHours(1);
LocalDateTime testLogoutTime = LocalDateTime.now().minusMinutes(30);
LocalDateTime testCreatedAt = LocalDateTime.now().minusDays(1);
LocalDateTime testUpdatedAt = LocalDateTime.now().minusHours(1);
LocalDateTime testDeletedAt = LocalDateTime.now();
log.setLoginTime(testLoginTime);
log.setLogoutTime(testLogoutTime);
log.setCreatedAt(testCreatedAt);
log.setUpdatedAt(testUpdatedAt);
log.setDeletedAt(testDeletedAt);
assertEquals(TEST_ID, log.getId());
assertEquals(TEST_CLIENT_USER_ID, log.getClientUserId());
assertEquals(TEST_IP_ADDRESS, log.getIpAddress());
assertEquals(testLoginTime, log.getLoginTime());
assertEquals(testLogoutTime, log.getLogoutTime());
assertEquals(testCreatedAt, log.getCreatedAt());
assertEquals(testUpdatedAt, log.getUpdatedAt());
assertEquals(testDeletedAt, log.getDeletedAt());
}
@Test
void testGenerateId_SetsUniqueId() {
ClientLoginLog log = new ClientLoginLog();
log.generateId();
assertNotNull(log.getId());
assertTrue(log.getId() > 0);
}
@Test
void testGenerateId_GeneratesDifferentIds() {
ClientLoginLog log1 = new ClientLoginLog();
ClientLoginLog log2 = new ClientLoginLog();
log1.generateId();
log2.generateId();
assertNotNull(log1.getId());
assertNotNull(log2.getId());
assertNotEquals(log1.getId(), log2.getId());
}
@Test
void testGenerateId_OverwritesExistingId() {
ClientLoginLog log = new ClientLoginLog();
log.setId(TEST_ID);
log.generateId();
assertNotEquals(TEST_ID, log.getId());
assertNotNull(log.getId());
}
@Test
void testDelete_SetsDeletedAt() {
ClientLoginLog log = new ClientLoginLog();
assertNull(log.getDeletedAt());
log.delete();
assertNotNull(log.getDeletedAt());
assertTrue(log.getDeletedAt().isBefore(LocalDateTime.now().plusSeconds(1)));
assertTrue(log.getDeletedAt().isAfter(LocalDateTime.now().minusSeconds(1)));
}
@Test
void testDelete_CanBeCalledMultipleTimes() {
ClientLoginLog log = new ClientLoginLog();
log.delete();
LocalDateTime firstDeletedAt = log.getDeletedAt();
log.delete();
LocalDateTime secondDeletedAt = log.getDeletedAt();
assertNotNull(secondDeletedAt);
assertNotEquals(firstDeletedAt, secondDeletedAt);
}
@Test
void testIsLogoutTimeNull_WhenNull() {
ClientLoginLog log = new ClientLoginLog();
log.setLogoutTime(null);
assertTrue(log.isLogoutTimeNull());
}
@Test
void testIsLogoutTimeNull_WhenNotNull() {
ClientLoginLog log = new ClientLoginLog();
LocalDateTime logoutTime = LocalDateTime.now();
log.setLogoutTime(logoutTime);
assertFalse(log.isLogoutTimeNull());
}
@Test
void testConstructor_GeneratesId() {
ClientLoginLog log1 = new ClientLoginLog(TEST_CLIENT_USER_ID, TEST_IP_ADDRESS);
ClientLoginLog log2 = new ClientLoginLog(TEST_CLIENT_USER_ID, TEST_IP_ADDRESS);
assertNotNull(log1.getId());
assertNotNull(log2.getId());
assertNotEquals(log1.getId(), log2.getId());
}
@Test
void testConstructor_SetsLoginTimeToNow() {
LocalDateTime before = LocalDateTime.now();
ClientLoginLog log = new ClientLoginLog(TEST_CLIENT_USER_ID, TEST_IP_ADDRESS);
LocalDateTime after = LocalDateTime.now();
assertNotNull(log.getLoginTime());
assertTrue(log.getLoginTime().isAfter(before.minusSeconds(1)));
assertTrue(log.getLoginTime().isBefore(after.plusSeconds(1)));
}
}
@@ -0,0 +1,209 @@
package io.destiny.client.core.domain;
import io.destiny.common.primitive.PhoneNumber;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class ClientUserTest {
private static final Long TEST_ID = 12345L;
private static final String TEST_USERNAME = "testuser";
private static final String TEST_PASSWORD = "password123";
private static final String TEST_WECHAT_OPEN_ID = "wx_open_id_123";
private static final String TEST_WECHAT_UNION_ID = "wx_union_id_456";
private static final String TEST_DOUYIN_OPEN_ID = "dy_open_id_789";
private static final String TEST_DOUYIN_UNION_ID = "dy_union_id_012";
private static final String TEST_GENDER = "male";
private static final String TEST_NICKNAME = "测试用户";
private static final String TEST_AVATAR_URL = "https://example.com/avatar.jpg";
@Test
void testConstructor_DefaultValues() {
ClientUser user = new ClientUser();
assertNull(user.getId());
assertNull(user.getUsername());
assertNull(user.getPassword());
assertNull(user.getEmail());
assertNull(user.getPhone());
assertNull(user.getGender());
assertNull(user.getBirthTime());
assertNull(user.getWechatOpenId());
assertNull(user.getWechatUnionId());
assertNull(user.getDouyinOpenId());
assertNull(user.getDouyinUnionId());
assertNull(user.getPhoneVerified());
assertNull(user.getNickname());
assertNull(user.getAvatarUrl());
assertNull(user.getCreatedAt());
assertNull(user.getUpdatedAt());
assertNull(user.getDeletedAt());
assertNull(user.getLoginLogs());
}
@Test
void testGettersAndSetters() {
ClientUser user = new ClientUser();
user.setId(TEST_ID);
user.setUsername(TEST_USERNAME);
user.setPassword(TEST_PASSWORD);
user.setGender(TEST_GENDER);
user.setWechatOpenId(TEST_WECHAT_OPEN_ID);
user.setWechatUnionId(TEST_WECHAT_UNION_ID);
user.setDouyinOpenId(TEST_DOUYIN_OPEN_ID);
user.setDouyinUnionId(TEST_DOUYIN_UNION_ID);
user.setPhoneVerified(true);
user.setNickname(TEST_NICKNAME);
user.setAvatarUrl(TEST_AVATAR_URL);
LocalDateTime testBirthTime = LocalDateTime.of(1990, 1, 1, 0, 0);
user.setBirthTime(testBirthTime);
LocalDateTime testCreatedAt = LocalDateTime.now().minusDays(1);
LocalDateTime testUpdatedAt = LocalDateTime.now().minusHours(1);
LocalDateTime testDeletedAt = LocalDateTime.now();
user.setCreatedAt(testCreatedAt);
user.setUpdatedAt(testUpdatedAt);
user.setDeletedAt(testDeletedAt);
assertEquals(TEST_ID, user.getId());
assertEquals(TEST_USERNAME, user.getUsername());
assertEquals(TEST_PASSWORD, user.getPassword());
assertEquals(TEST_GENDER, user.getGender());
assertEquals(TEST_WECHAT_OPEN_ID, user.getWechatOpenId());
assertEquals(TEST_WECHAT_UNION_ID, user.getWechatUnionId());
assertEquals(TEST_DOUYIN_OPEN_ID, user.getDouyinOpenId());
assertEquals(TEST_DOUYIN_UNION_ID, user.getDouyinUnionId());
assertTrue(user.getPhoneVerified());
assertEquals(TEST_NICKNAME, user.getNickname());
assertEquals(TEST_AVATAR_URL, user.getAvatarUrl());
assertEquals(testBirthTime, user.getBirthTime());
assertEquals(testCreatedAt, user.getCreatedAt());
assertEquals(testUpdatedAt, user.getUpdatedAt());
assertEquals(testDeletedAt, user.getDeletedAt());
}
@Test
void testSetPhone_WithPhoneNumber() {
ClientUser user = new ClientUser();
PhoneNumber phoneNumber = PhoneNumber.of("13800138000");
user.setPhone(phoneNumber);
assertEquals(phoneNumber, user.getPhone());
}
@Test
void testSetPhone_WithNull() {
ClientUser user = new ClientUser();
user.setPhone(null);
assertNull(user.getPhone());
}
@Test
void testGenerateId_SetsUniqueId() {
ClientUser user = new ClientUser();
user.generateId();
assertNotNull(user.getId());
assertTrue(user.getId() > 0);
}
@Test
void testGenerateId_GeneratesDifferentIds() {
ClientUser user1 = new ClientUser();
ClientUser user2 = new ClientUser();
user1.generateId();
user2.generateId();
assertNotNull(user1.getId());
assertNotNull(user2.getId());
assertNotEquals(user1.getId(), user2.getId());
}
@Test
void testDelete_SetsDeletedAt() {
ClientUser user = new ClientUser();
assertNull(user.getDeletedAt());
user.delete();
assertNotNull(user.getDeletedAt());
assertTrue(user.getDeletedAt().isBefore(LocalDateTime.now().plusSeconds(1)));
assertTrue(user.getDeletedAt().isAfter(LocalDateTime.now().minusSeconds(1)));
}
@Test
void testDelete_CanBeCalledMultipleTimes() {
ClientUser user = new ClientUser();
user.delete();
LocalDateTime firstDeletedAt = user.getDeletedAt();
user.delete();
LocalDateTime secondDeletedAt = user.getDeletedAt();
assertNotNull(secondDeletedAt);
assertNotEquals(firstDeletedAt, secondDeletedAt);
}
@Test
void testPhoneVerified_DefaultValue() {
ClientUser user = new ClientUser();
assertNull(user.getPhoneVerified());
}
@Test
void testPhoneVerified_SetToTrue() {
ClientUser user = new ClientUser();
user.setPhoneVerified(true);
assertTrue(user.getPhoneVerified());
}
@Test
void testPhoneVerified_SetToFalse() {
ClientUser user = new ClientUser();
user.setPhoneVerified(false);
assertFalse(user.getPhoneVerified());
}
@Test
void testDeletedAt_DefaultValue() {
ClientUser user = new ClientUser();
assertNull(user.getDeletedAt());
}
@Test
void testDeleted_AfterDelete() {
ClientUser user = new ClientUser();
user.delete();
assertNotNull(user.getDeletedAt());
}
@Test
void testLoginLogs_DefaultValue() {
ClientUser user = new ClientUser();
assertNull(user.getLoginLogs());
}
@Test
void testSetLoginLogs_WithEmptyList() {
ClientUser user = new ClientUser();
user.setLoginLogs(List.of());
assertNotNull(user.getLoginLogs());
assertTrue(user.getLoginLogs().isEmpty());
}
}
@@ -0,0 +1,163 @@
package io.destiny.client.core.domain;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.*;
class SmsVerificationCodeTest {
private static final String TEST_PHONE = "13800138000";
private static final String TEST_CODE = "123456";
private static final String TEST_TYPE = "login";
@Test
void testConstructor_ValidInput_Success() {
LocalDateTime expireTime = LocalDateTime.now().plusMinutes(5);
SmsVerificationCode code = new SmsVerificationCode(TEST_PHONE, TEST_CODE, TEST_TYPE, expireTime);
assertNotNull(code.getId());
assertEquals(TEST_PHONE, code.getPhone());
assertEquals(TEST_CODE, code.getCode());
assertEquals(TEST_TYPE, code.getType());
assertEquals(expireTime, code.getExpireTime());
assertNotNull(code.getCreatedAt());
assertNotNull(code.getUpdatedAt());
assertFalse(code.isVerified());
assertEquals(0, code.getVerifyCount());
}
@Test
void testConstructor_GeneratesUniqueId() {
LocalDateTime expireTime = LocalDateTime.now().plusMinutes(5);
SmsVerificationCode code1 = new SmsVerificationCode(TEST_PHONE, TEST_CODE, TEST_TYPE, expireTime);
SmsVerificationCode code2 = new SmsVerificationCode(TEST_PHONE, TEST_CODE, TEST_TYPE, expireTime);
assertNotNull(code1.getId());
assertNotNull(code2.getId());
assertNotEquals(code1.getId(), code2.getId());
}
@Test
void testMarkAsVerified_UpdatesStatusAndTimestamp() {
LocalDateTime expireTime = LocalDateTime.now().plusMinutes(5);
SmsVerificationCode code = new SmsVerificationCode(TEST_PHONE, TEST_CODE, TEST_TYPE, expireTime);
LocalDateTime originalUpdatedAt = code.getUpdatedAt();
code.markAsVerified();
assertTrue(code.isVerified());
assertTrue(code.getUpdatedAt().isAfter(originalUpdatedAt) || code.getUpdatedAt().isEqual(originalUpdatedAt));
}
@Test
void testIncrementVerifyCount_FromZero() {
LocalDateTime expireTime = LocalDateTime.now().plusMinutes(5);
SmsVerificationCode code = new SmsVerificationCode(TEST_PHONE, TEST_CODE, TEST_TYPE, expireTime);
code.incrementVerifyCount();
assertEquals(1, code.getVerifyCount());
}
@Test
void testIncrementVerifyCount_MultipleTimes() {
LocalDateTime expireTime = LocalDateTime.now().plusMinutes(5);
SmsVerificationCode code = new SmsVerificationCode(TEST_PHONE, TEST_CODE, TEST_TYPE, expireTime);
code.incrementVerifyCount();
code.incrementVerifyCount();
code.incrementVerifyCount();
assertEquals(3, code.getVerifyCount());
}
@Test
void testIncrementVerifyCount_UpdatesTimestamp() {
LocalDateTime expireTime = LocalDateTime.now().plusMinutes(5);
SmsVerificationCode code = new SmsVerificationCode(TEST_PHONE, TEST_CODE, TEST_TYPE, expireTime);
LocalDateTime originalUpdatedAt = code.getUpdatedAt();
code.incrementVerifyCount();
assertTrue(code.getUpdatedAt().isAfter(originalUpdatedAt) || code.getUpdatedAt().isEqual(originalUpdatedAt));
}
@Test
void testIsExpired_WithFutureExpireTime() {
LocalDateTime expireTime = LocalDateTime.now().plusMinutes(5);
SmsVerificationCode code = new SmsVerificationCode(TEST_PHONE, TEST_CODE, TEST_TYPE, expireTime);
assertFalse(code.isExpired());
}
@Test
void testIsExpired_WithPastExpireTime() {
LocalDateTime expireTime = LocalDateTime.now().minusMinutes(5);
SmsVerificationCode code = new SmsVerificationCode(TEST_PHONE, TEST_CODE, TEST_TYPE, expireTime);
assertTrue(code.isExpired());
}
@Test
void testIsExpired_WithCurrentTime() {
LocalDateTime expireTime = LocalDateTime.now().plusSeconds(1);
SmsVerificationCode code = new SmsVerificationCode(TEST_PHONE, TEST_CODE, TEST_TYPE, expireTime);
assertFalse(code.isExpired());
}
@Test
void testIsVerified_DefaultValue() {
LocalDateTime expireTime = LocalDateTime.now().plusMinutes(5);
SmsVerificationCode code = new SmsVerificationCode(TEST_PHONE, TEST_CODE, TEST_TYPE, expireTime);
assertFalse(code.isVerified());
}
@Test
void testIsVerified_AfterMarkAsVerified() {
LocalDateTime expireTime = LocalDateTime.now().plusMinutes(5);
SmsVerificationCode code = new SmsVerificationCode(TEST_PHONE, TEST_CODE, TEST_TYPE, expireTime);
code.markAsVerified();
assertTrue(code.isVerified());
}
@Test
void testGetVerifyCount_DefaultValue() {
LocalDateTime expireTime = LocalDateTime.now().plusMinutes(5);
SmsVerificationCode code = new SmsVerificationCode(TEST_PHONE, TEST_CODE, TEST_TYPE, expireTime);
assertEquals(0, code.getVerifyCount());
}
@Test
void testGetVerifyCount_AfterIncrements() {
LocalDateTime expireTime = LocalDateTime.now().plusMinutes(5);
SmsVerificationCode code = new SmsVerificationCode(TEST_PHONE, TEST_CODE, TEST_TYPE, expireTime);
code.incrementVerifyCount();
code.incrementVerifyCount();
assertEquals(2, code.getVerifyCount());
}
@Test
void testGettersAndSetters() {
LocalDateTime expireTime = LocalDateTime.now().plusMinutes(5);
SmsVerificationCode code = new SmsVerificationCode(TEST_PHONE, TEST_CODE, TEST_TYPE, expireTime);
Long testId = 12345L;
LocalDateTime testCreatedAt = LocalDateTime.now().minusHours(1);
LocalDateTime testUpdatedAt = LocalDateTime.now().minusMinutes(30);
code.setId(testId);
code.setCreatedAt(testCreatedAt);
code.setUpdatedAt(testUpdatedAt);
assertEquals(testId, code.getId());
assertEquals(testCreatedAt, code.getCreatedAt());
assertEquals(testUpdatedAt, code.getUpdatedAt());
}
}
@@ -0,0 +1,167 @@
package io.destiny.client.core.domain;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.*;
class UserBindingTest {
private static final Long TEST_ID = 12345L;
private static final Long TEST_USER_ID = 67890L;
private static final String TEST_PLATFORM = "wechat";
private static final String TEST_PLATFORM_OPEN_ID = "wx_open_id_123";
private static final String TEST_PLATFORM_UNION_ID = "wx_union_id_456";
@Test
void testConstructor_DefaultValues() {
UserBinding binding = new UserBinding();
assertNull(binding.getId());
assertNull(binding.getUserId());
assertNull(binding.getPlatform());
assertNull(binding.getPlatformOpenId());
assertNull(binding.getPlatformUnionId());
assertNull(binding.getCreatedAt());
assertNull(binding.getUpdatedAt());
assertNull(binding.getDeletedAt());
}
@Test
void testGettersAndSetters() {
UserBinding binding = new UserBinding();
binding.setId(TEST_ID);
binding.setUserId(TEST_USER_ID);
binding.setPlatform(TEST_PLATFORM);
binding.setPlatformOpenId(TEST_PLATFORM_OPEN_ID);
binding.setPlatformUnionId(TEST_PLATFORM_UNION_ID);
LocalDateTime testCreatedAt = LocalDateTime.now().minusDays(1);
LocalDateTime testUpdatedAt = LocalDateTime.now().minusHours(1);
LocalDateTime testDeletedAt = LocalDateTime.now();
binding.setCreatedAt(testCreatedAt);
binding.setUpdatedAt(testUpdatedAt);
binding.setDeletedAt(testDeletedAt);
assertEquals(TEST_ID, binding.getId());
assertEquals(TEST_USER_ID, binding.getUserId());
assertEquals(TEST_PLATFORM, binding.getPlatform());
assertEquals(TEST_PLATFORM_OPEN_ID, binding.getPlatformOpenId());
assertEquals(TEST_PLATFORM_UNION_ID, binding.getPlatformUnionId());
assertEquals(testCreatedAt, binding.getCreatedAt());
assertEquals(testUpdatedAt, binding.getUpdatedAt());
assertEquals(testDeletedAt, binding.getDeletedAt());
}
@Test
void testGenerateId_SetsUniqueId() {
UserBinding binding = new UserBinding();
binding.generateId();
assertNotNull(binding.getId());
assertTrue(binding.getId() > 0);
}
@Test
void testGenerateId_GeneratesDifferentIds() {
UserBinding binding1 = new UserBinding();
UserBinding binding2 = new UserBinding();
binding1.generateId();
binding2.generateId();
assertNotNull(binding1.getId());
assertNotNull(binding2.getId());
assertNotEquals(binding1.getId(), binding2.getId());
}
@Test
void testDelete_SetsDeletedAt() {
UserBinding binding = new UserBinding();
assertNull(binding.getDeletedAt());
binding.delete();
assertNotNull(binding.getDeletedAt());
assertTrue(binding.getDeletedAt().isBefore(LocalDateTime.now().plusSeconds(1)));
assertTrue(binding.getDeletedAt().isAfter(LocalDateTime.now().minusSeconds(1)));
}
@Test
void testDelete_CanBeCalledMultipleTimes() {
UserBinding binding = new UserBinding();
binding.delete();
LocalDateTime firstDeletedAt = binding.getDeletedAt();
binding.delete();
LocalDateTime secondDeletedAt = binding.getDeletedAt();
assertNotNull(secondDeletedAt);
assertNotEquals(firstDeletedAt, secondDeletedAt);
}
@Test
void testIsWechatPlatform_WhenWechat() {
UserBinding binding = new UserBinding();
binding.setPlatform("wechat");
assertTrue(binding.isWechatPlatform());
}
@Test
void testIsWechatPlatform_WhenDouyin() {
UserBinding binding = new UserBinding();
binding.setPlatform("douyin");
assertFalse(binding.isWechatPlatform());
}
@Test
void testIsWechatPlatform_WhenNull() {
UserBinding binding = new UserBinding();
binding.setPlatform(null);
assertFalse(binding.isWechatPlatform());
}
@Test
void testIsDouyinPlatform_WhenDouyin() {
UserBinding binding = new UserBinding();
binding.setPlatform("douyin");
assertTrue(binding.isDouyinPlatform());
}
@Test
void testIsDouyinPlatform_WhenWechat() {
UserBinding binding = new UserBinding();
binding.setPlatform("wechat");
assertFalse(binding.isDouyinPlatform());
}
@Test
void testIsDouyinPlatform_WhenNull() {
UserBinding binding = new UserBinding();
binding.setPlatform(null);
assertFalse(binding.isDouyinPlatform());
}
@Test
void testIsDeleted_WhenDeletedAtPresent() {
UserBinding binding = new UserBinding();
binding.setDeletedAt(LocalDateTime.now());
assertTrue(binding.isDeleted());
}
@Test
void testIsDeleted_WhenDeletedAtNull() {
UserBinding binding = new UserBinding();
binding.setDeletedAt(null);
assertFalse(binding.isDeleted());
}
}
@@ -0,0 +1,49 @@
package io.destiny.client.core.enums;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class PlatformTypeTest {
@Test
void testGetCode() {
assertEquals("wechat", PlatformType.WECHAT.getCode());
assertEquals("douyin", PlatformType.DOUYIN.getCode());
}
@Test
void testGetDisplayName() {
assertEquals("微信小程序", PlatformType.WECHAT.getDisplayName());
assertEquals("抖音小程序", PlatformType.DOUYIN.getDisplayName());
}
@Test
void testFromCode() {
assertEquals(PlatformType.WECHAT, PlatformType.fromCode("wechat"));
assertEquals(PlatformType.DOUYIN, PlatformType.fromCode("douyin"));
}
@Test
void testFromCodeInvalid() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> PlatformType.fromCode("invalid_code")
);
assertTrue(exception.getMessage().contains("Unknown platform type"));
}
@Test
void testValues() {
PlatformType[] values = PlatformType.values();
assertEquals(2, values.length);
assertEquals(PlatformType.WECHAT, values[0]);
assertEquals(PlatformType.DOUYIN, values[1]);
}
@Test
void testValueOf() {
assertEquals(PlatformType.WECHAT, PlatformType.valueOf("WECHAT"));
assertEquals(PlatformType.DOUYIN, PlatformType.valueOf("DOUYIN"));
}
}
@@ -0,0 +1,67 @@
package io.destiny.client.core.enums;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class SmsCodeTypeTest {
@Test
void testGetCode() {
assertEquals("login", SmsCodeType.LOGIN.getCode());
assertEquals("register", SmsCodeType.REGISTER.getCode());
assertEquals("bind", SmsCodeType.BIND.getCode());
assertEquals("reset_password", SmsCodeType.RESET_PASSWORD.getCode());
}
@Test
void testGetDisplayName() {
assertEquals("登录验证码", SmsCodeType.LOGIN.getDisplayName());
assertEquals("注册验证码", SmsCodeType.REGISTER.getDisplayName());
assertEquals("绑定验证码", SmsCodeType.BIND.getDisplayName());
assertEquals("重置密码验证码", SmsCodeType.RESET_PASSWORD.getDisplayName());
}
@Test
void testGetTemplateCode() {
assertEquals("SMS_123456789", SmsCodeType.LOGIN.getTemplateCode());
assertEquals("SMS_123456790", SmsCodeType.REGISTER.getTemplateCode());
assertEquals("SMS_123456791", SmsCodeType.BIND.getTemplateCode());
assertEquals("SMS_123456792", SmsCodeType.RESET_PASSWORD.getTemplateCode());
}
@Test
void testFromCode() {
assertEquals(SmsCodeType.LOGIN, SmsCodeType.fromCode("login"));
assertEquals(SmsCodeType.REGISTER, SmsCodeType.fromCode("register"));
assertEquals(SmsCodeType.BIND, SmsCodeType.fromCode("bind"));
assertEquals(SmsCodeType.RESET_PASSWORD, SmsCodeType.fromCode("reset_password"));
}
@Test
void testFromCodeInvalid() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> SmsCodeType.fromCode("invalid_code")
);
assertTrue(exception.getMessage().contains("Unknown SMS code type"));
}
@Test
void testValues() {
SmsCodeType[] values = SmsCodeType.values();
assertEquals(4, values.length);
assertEquals(SmsCodeType.LOGIN, values[0]);
assertEquals(SmsCodeType.REGISTER, values[1]);
assertEquals(SmsCodeType.BIND, values[2]);
assertEquals(SmsCodeType.RESET_PASSWORD, values[3]);
}
@Test
void testValueOf() {
assertEquals(SmsCodeType.LOGIN, SmsCodeType.valueOf("LOGIN"));
assertEquals(SmsCodeType.REGISTER, SmsCodeType.valueOf("REGISTER"));
assertEquals(SmsCodeType.BIND, SmsCodeType.valueOf("BIND"));
assertEquals(SmsCodeType.RESET_PASSWORD, SmsCodeType.valueOf("RESET_PASSWORD"));
}
}
@@ -0,0 +1,130 @@
package io.destiny.client.core.exception;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ClientUserAuthExceptionCodeTest {
@Test
void testGetCode() {
assertEquals("CLIENT_AUTH_10001", ClientUserAuthExceptionCode.CLIENT_INVALID_USERNAME_OR_PASSWORD.getCode());
assertEquals("CLIENT_AUTH_10002", ClientUserAuthExceptionCode.CLIENT_INVALID_PLATFORM.getCode());
assertEquals("CLIENT_AUTH_10003", ClientUserAuthExceptionCode.CLIENT_INVALID_WECHAT_CODE.getCode());
assertEquals("CLIENT_AUTH_10004", ClientUserAuthExceptionCode.CLIENT_WECHAT_AUTH_FAILED.getCode());
assertEquals("CLIENT_AUTH_10005", ClientUserAuthExceptionCode.CLIENT_INVALID_DOUYIN_CODE.getCode());
assertEquals("CLIENT_AUTH_10006", ClientUserAuthExceptionCode.CLIENT_DOUYIN_AUTH_FAILED.getCode());
assertEquals("CLIENT_AUTH_10007", ClientUserAuthExceptionCode.CLIENT_SMS_CODE_EXPIRED.getCode());
assertEquals("CLIENT_AUTH_10008", ClientUserAuthExceptionCode.CLIENT_SMS_CODE_INVALID.getCode());
assertEquals("CLIENT_AUTH_10009", ClientUserAuthExceptionCode.CLIENT_SMS_CODE_USED.getCode());
assertEquals("CLIENT_AUTH_10010", ClientUserAuthExceptionCode.CLIENT_SMS_CODE_EXCEEDED.getCode());
assertEquals("CLIENT_AUTH_10011", ClientUserAuthExceptionCode.CLIENT_SMS_CODE_SEND_TOO_FAST.getCode());
assertEquals("CLIENT_AUTH_10012", ClientUserAuthExceptionCode.CLIENT_SMS_CODE_SEND_EXCEEDED.getCode());
assertEquals("CLIENT_AUTH_10013", ClientUserAuthExceptionCode.CLIENT_USER_NOT_FOUND.getCode());
assertEquals("CLIENT_AUTH_10014", ClientUserAuthExceptionCode.CLIENT_ACCOUNT_ALREADY_BOUND.getCode());
assertEquals("CLIENT_AUTH_10015", ClientUserAuthExceptionCode.CLIENT_ACCOUNT_BIND_FAILED.getCode());
assertEquals("CLIENT_AUTH_10016", ClientUserAuthExceptionCode.CLIENT_PHONE_NOT_VERIFIED.getCode());
assertEquals("CLIENT_AUTH_10017", ClientUserAuthExceptionCode.CLIENT_AUTO_REGISTER_FAILED.getCode());
assertEquals("CLIENT_AUTH_10018", ClientUserAuthExceptionCode.CLIENT_LOGIN_TYPE_INVALID.getCode());
assertEquals("CLIENT_AUTH_10019", ClientUserAuthExceptionCode.CLIENT_LOGIN_FAILED.getCode());
assertEquals("CLIENT_AUTH_10020", ClientUserAuthExceptionCode.CLIENT_ACCOUNT_LOCKED.getCode());
}
@Test
void testGetDisplayName() {
assertEquals("用户名或密码错误", ClientUserAuthExceptionCode.CLIENT_INVALID_USERNAME_OR_PASSWORD.getDisplayName());
assertEquals("平台标识无效", ClientUserAuthExceptionCode.CLIENT_INVALID_PLATFORM.getDisplayName());
assertEquals("微信授权码无效", ClientUserAuthExceptionCode.CLIENT_INVALID_WECHAT_CODE.getDisplayName());
assertEquals("微信授权失败", ClientUserAuthExceptionCode.CLIENT_WECHAT_AUTH_FAILED.getDisplayName());
assertEquals("抖音授权码无效", ClientUserAuthExceptionCode.CLIENT_INVALID_DOUYIN_CODE.getDisplayName());
assertEquals("抖音授权失败", ClientUserAuthExceptionCode.CLIENT_DOUYIN_AUTH_FAILED.getDisplayName());
assertEquals("验证码已过期", ClientUserAuthExceptionCode.CLIENT_SMS_CODE_EXPIRED.getDisplayName());
assertEquals("验证码错误", ClientUserAuthExceptionCode.CLIENT_SMS_CODE_INVALID.getDisplayName());
assertEquals("验证码已使用", ClientUserAuthExceptionCode.CLIENT_SMS_CODE_USED.getDisplayName());
assertEquals("验证码验证次数过多", ClientUserAuthExceptionCode.CLIENT_SMS_CODE_EXCEEDED.getDisplayName());
assertEquals("验证码发送频率过高", ClientUserAuthExceptionCode.CLIENT_SMS_CODE_SEND_TOO_FAST.getDisplayName());
assertEquals("验证码发送次数超限", ClientUserAuthExceptionCode.CLIENT_SMS_CODE_SEND_EXCEEDED.getDisplayName());
assertEquals("用户不存在", ClientUserAuthExceptionCode.CLIENT_USER_NOT_FOUND.getDisplayName());
assertEquals("账号已绑定", ClientUserAuthExceptionCode.CLIENT_ACCOUNT_ALREADY_BOUND.getDisplayName());
assertEquals("账号绑定失败", ClientUserAuthExceptionCode.CLIENT_ACCOUNT_BIND_FAILED.getDisplayName());
assertEquals("手机号未验证", ClientUserAuthExceptionCode.CLIENT_PHONE_NOT_VERIFIED.getDisplayName());
assertEquals("自动注册失败", ClientUserAuthExceptionCode.CLIENT_AUTO_REGISTER_FAILED.getDisplayName());
assertEquals("登录类型无效", ClientUserAuthExceptionCode.CLIENT_LOGIN_TYPE_INVALID.getDisplayName());
assertEquals("登录失败", ClientUserAuthExceptionCode.CLIENT_LOGIN_FAILED.getDisplayName());
assertEquals("账号已锁定", ClientUserAuthExceptionCode.CLIENT_ACCOUNT_LOCKED.getDisplayName());
}
@Test
void testGetHttpCode() {
assertEquals("401", ClientUserAuthExceptionCode.CLIENT_INVALID_USERNAME_OR_PASSWORD.getHttpCode());
assertEquals("400", ClientUserAuthExceptionCode.CLIENT_INVALID_PLATFORM.getHttpCode());
assertEquals("400", ClientUserAuthExceptionCode.CLIENT_INVALID_WECHAT_CODE.getHttpCode());
assertEquals("400", ClientUserAuthExceptionCode.CLIENT_WECHAT_AUTH_FAILED.getHttpCode());
assertEquals("400", ClientUserAuthExceptionCode.CLIENT_INVALID_DOUYIN_CODE.getHttpCode());
assertEquals("400", ClientUserAuthExceptionCode.CLIENT_DOUYIN_AUTH_FAILED.getHttpCode());
assertEquals("400", ClientUserAuthExceptionCode.CLIENT_SMS_CODE_EXPIRED.getHttpCode());
assertEquals("400", ClientUserAuthExceptionCode.CLIENT_SMS_CODE_INVALID.getHttpCode());
assertEquals("400", ClientUserAuthExceptionCode.CLIENT_SMS_CODE_USED.getHttpCode());
assertEquals("400", ClientUserAuthExceptionCode.CLIENT_SMS_CODE_EXCEEDED.getHttpCode());
assertEquals("429", ClientUserAuthExceptionCode.CLIENT_SMS_CODE_SEND_TOO_FAST.getHttpCode());
assertEquals("429", ClientUserAuthExceptionCode.CLIENT_SMS_CODE_SEND_EXCEEDED.getHttpCode());
assertEquals("404", ClientUserAuthExceptionCode.CLIENT_USER_NOT_FOUND.getHttpCode());
assertEquals("400", ClientUserAuthExceptionCode.CLIENT_ACCOUNT_ALREADY_BOUND.getHttpCode());
assertEquals("400", ClientUserAuthExceptionCode.CLIENT_ACCOUNT_BIND_FAILED.getHttpCode());
assertEquals("400", ClientUserAuthExceptionCode.CLIENT_PHONE_NOT_VERIFIED.getHttpCode());
assertEquals("400", ClientUserAuthExceptionCode.CLIENT_AUTO_REGISTER_FAILED.getHttpCode());
assertEquals("400", ClientUserAuthExceptionCode.CLIENT_LOGIN_TYPE_INVALID.getHttpCode());
assertEquals("401", ClientUserAuthExceptionCode.CLIENT_LOGIN_FAILED.getHttpCode());
assertEquals("403", ClientUserAuthExceptionCode.CLIENT_ACCOUNT_LOCKED.getHttpCode());
}
@Test
void testValues() {
ClientUserAuthExceptionCode[] values = ClientUserAuthExceptionCode.values();
assertEquals(20, values.length);
assertEquals(ClientUserAuthExceptionCode.CLIENT_INVALID_USERNAME_OR_PASSWORD, values[0]);
assertEquals(ClientUserAuthExceptionCode.CLIENT_INVALID_PLATFORM, values[1]);
assertEquals(ClientUserAuthExceptionCode.CLIENT_INVALID_WECHAT_CODE, values[2]);
assertEquals(ClientUserAuthExceptionCode.CLIENT_WECHAT_AUTH_FAILED, values[3]);
assertEquals(ClientUserAuthExceptionCode.CLIENT_INVALID_DOUYIN_CODE, values[4]);
assertEquals(ClientUserAuthExceptionCode.CLIENT_DOUYIN_AUTH_FAILED, values[5]);
assertEquals(ClientUserAuthExceptionCode.CLIENT_SMS_CODE_EXPIRED, values[6]);
assertEquals(ClientUserAuthExceptionCode.CLIENT_SMS_CODE_INVALID, values[7]);
assertEquals(ClientUserAuthExceptionCode.CLIENT_SMS_CODE_USED, values[8]);
assertEquals(ClientUserAuthExceptionCode.CLIENT_SMS_CODE_EXCEEDED, values[9]);
assertEquals(ClientUserAuthExceptionCode.CLIENT_SMS_CODE_SEND_TOO_FAST, values[10]);
assertEquals(ClientUserAuthExceptionCode.CLIENT_SMS_CODE_SEND_EXCEEDED, values[11]);
assertEquals(ClientUserAuthExceptionCode.CLIENT_USER_NOT_FOUND, values[12]);
assertEquals(ClientUserAuthExceptionCode.CLIENT_ACCOUNT_ALREADY_BOUND, values[13]);
assertEquals(ClientUserAuthExceptionCode.CLIENT_ACCOUNT_BIND_FAILED, values[14]);
assertEquals(ClientUserAuthExceptionCode.CLIENT_PHONE_NOT_VERIFIED, values[15]);
assertEquals(ClientUserAuthExceptionCode.CLIENT_AUTO_REGISTER_FAILED, values[16]);
assertEquals(ClientUserAuthExceptionCode.CLIENT_LOGIN_TYPE_INVALID, values[17]);
assertEquals(ClientUserAuthExceptionCode.CLIENT_LOGIN_FAILED, values[18]);
assertEquals(ClientUserAuthExceptionCode.CLIENT_ACCOUNT_LOCKED, values[19]);
}
@Test
void testValueOf() {
assertEquals(ClientUserAuthExceptionCode.CLIENT_INVALID_USERNAME_OR_PASSWORD, ClientUserAuthExceptionCode.valueOf("CLIENT_INVALID_USERNAME_OR_PASSWORD"));
assertEquals(ClientUserAuthExceptionCode.CLIENT_INVALID_PLATFORM, ClientUserAuthExceptionCode.valueOf("CLIENT_INVALID_PLATFORM"));
assertEquals(ClientUserAuthExceptionCode.CLIENT_INVALID_WECHAT_CODE, ClientUserAuthExceptionCode.valueOf("CLIENT_INVALID_WECHAT_CODE"));
assertEquals(ClientUserAuthExceptionCode.CLIENT_WECHAT_AUTH_FAILED, ClientUserAuthExceptionCode.valueOf("CLIENT_WECHAT_AUTH_FAILED"));
assertEquals(ClientUserAuthExceptionCode.CLIENT_INVALID_DOUYIN_CODE, ClientUserAuthExceptionCode.valueOf("CLIENT_INVALID_DOUYIN_CODE"));
assertEquals(ClientUserAuthExceptionCode.CLIENT_DOUYIN_AUTH_FAILED, ClientUserAuthExceptionCode.valueOf("CLIENT_DOUYIN_AUTH_FAILED"));
assertEquals(ClientUserAuthExceptionCode.CLIENT_SMS_CODE_EXPIRED, ClientUserAuthExceptionCode.valueOf("CLIENT_SMS_CODE_EXPIRED"));
assertEquals(ClientUserAuthExceptionCode.CLIENT_SMS_CODE_INVALID, ClientUserAuthExceptionCode.valueOf("CLIENT_SMS_CODE_INVALID"));
assertEquals(ClientUserAuthExceptionCode.CLIENT_SMS_CODE_USED, ClientUserAuthExceptionCode.valueOf("CLIENT_SMS_CODE_USED"));
assertEquals(ClientUserAuthExceptionCode.CLIENT_SMS_CODE_EXCEEDED, ClientUserAuthExceptionCode.valueOf("CLIENT_SMS_CODE_EXCEEDED"));
assertEquals(ClientUserAuthExceptionCode.CLIENT_SMS_CODE_SEND_TOO_FAST, ClientUserAuthExceptionCode.valueOf("CLIENT_SMS_CODE_SEND_TOO_FAST"));
assertEquals(ClientUserAuthExceptionCode.CLIENT_SMS_CODE_SEND_EXCEEDED, ClientUserAuthExceptionCode.valueOf("CLIENT_SMS_CODE_SEND_EXCEEDED"));
assertEquals(ClientUserAuthExceptionCode.CLIENT_USER_NOT_FOUND, ClientUserAuthExceptionCode.valueOf("CLIENT_USER_NOT_FOUND"));
assertEquals(ClientUserAuthExceptionCode.CLIENT_ACCOUNT_ALREADY_BOUND, ClientUserAuthExceptionCode.valueOf("CLIENT_ACCOUNT_ALREADY_BOUND"));
assertEquals(ClientUserAuthExceptionCode.CLIENT_ACCOUNT_BIND_FAILED, ClientUserAuthExceptionCode.valueOf("CLIENT_ACCOUNT_BIND_FAILED"));
assertEquals(ClientUserAuthExceptionCode.CLIENT_PHONE_NOT_VERIFIED, ClientUserAuthExceptionCode.valueOf("CLIENT_PHONE_NOT_VERIFIED"));
assertEquals(ClientUserAuthExceptionCode.CLIENT_AUTO_REGISTER_FAILED, ClientUserAuthExceptionCode.valueOf("CLIENT_AUTO_REGISTER_FAILED"));
assertEquals(ClientUserAuthExceptionCode.CLIENT_LOGIN_TYPE_INVALID, ClientUserAuthExceptionCode.valueOf("CLIENT_LOGIN_TYPE_INVALID"));
assertEquals(ClientUserAuthExceptionCode.CLIENT_LOGIN_FAILED, ClientUserAuthExceptionCode.valueOf("CLIENT_LOGIN_FAILED"));
assertEquals(ClientUserAuthExceptionCode.CLIENT_ACCOUNT_LOCKED, ClientUserAuthExceptionCode.valueOf("CLIENT_ACCOUNT_LOCKED"));
}
}
@@ -0,0 +1,35 @@
package io.destiny.client.core.exception;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ClientUserExceptionCodeTest {
@Test
void testGetCode() {
assertEquals("CLIENT_USER_20001", ClientUserExceptionCode.CLIENT_USER_NOT_FOUND.getCode());
}
@Test
void testGetDisplayName() {
assertEquals("客户端用户不存在", ClientUserExceptionCode.CLIENT_USER_NOT_FOUND.getDisplayName());
}
@Test
void testGetHttpCode() {
assertEquals("500", ClientUserExceptionCode.CLIENT_USER_NOT_FOUND.getHttpCode());
}
@Test
void testValues() {
ClientUserExceptionCode[] values = ClientUserExceptionCode.values();
assertEquals(1, values.length);
assertEquals(ClientUserExceptionCode.CLIENT_USER_NOT_FOUND, values[0]);
}
@Test
void testValueOf() {
assertEquals(ClientUserExceptionCode.CLIENT_USER_NOT_FOUND, ClientUserExceptionCode.valueOf("CLIENT_USER_NOT_FOUND"));
}
}
@@ -0,0 +1,157 @@
package io.destiny.client.core.exception;
import io.destiny.common.exception.ApplicationException;
import io.destiny.common.exception.ExceptionCode;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
import reactor.test.StepVerifier;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(MockitoExtension.class)
@DisplayName("GlobalExceptionHandler 测试")
class GlobalExceptionHandlerTest {
private GlobalExceptionHandler globalExceptionHandler;
private ServerWebExchange exchange;
@BeforeEach
void setUp() {
globalExceptionHandler = new GlobalExceptionHandler();
exchange = MockServerWebExchange.from(org.springframework.mock.http.server.reactive.MockServerHttpRequest
.get("/api/test").accept(MediaType.APPLICATION_JSON));
}
@Test
@DisplayName("处理 ApplicationException - 包含 exceptionCode")
void testHandleApplicationExceptionWithCode() {
ApplicationException exception = new ApplicationException(
ClientUserAuthExceptionCode.CLIENT_SMS_CODE_INVALID);
StepVerifier.create(globalExceptionHandler.handleException(exception, exchange))
.assertNext(response -> {
assertEquals(HttpStatus.BAD_REQUEST, response.statusCode());
})
.verifyComplete();
}
@Test
@DisplayName("处理 ApplicationException - exceptionCode 为 nullexceptionMessage 不为 null")
void testHandleApplicationExceptionWithNullCodeAndMessage() {
ApplicationException exception = new ApplicationException((ExceptionCode) null,
new Object[] { "Custom error message" });
StepVerifier.create(globalExceptionHandler.handleException(exception, exchange))
.assertNext(response -> {
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.statusCode());
})
.verifyComplete();
}
@Test
@DisplayName("处理 ApplicationException - exceptionCode 不为 nullarguments 为 null")
void testHandleApplicationExceptionWithCodeAndNullMessage() {
ApplicationException exception = new ApplicationException(
ClientUserAuthExceptionCode.CLIENT_SMS_CODE_INVALID,
(Object[]) null);
StepVerifier.create(globalExceptionHandler.handleException(exception, exchange))
.assertNext(response -> {
assertEquals(HttpStatus.BAD_REQUEST, response.statusCode());
})
.verifyComplete();
}
@Test
@DisplayName("处理 ApplicationException - exceptionCode 和 arguments 都为 null")
void testHandleApplicationExceptionWithNullCodeAndNullMessage() {
ApplicationException exception = new ApplicationException((ExceptionCode) null, (Object[]) null);
StepVerifier.create(globalExceptionHandler.handleException(exception, exchange))
.assertNext(response -> {
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.statusCode());
})
.verifyComplete();
}
@Test
@DisplayName("处理 IllegalArgumentException")
void testHandleIllegalArgumentException() {
IllegalArgumentException exception = new IllegalArgumentException("Invalid argument");
StepVerifier.create(globalExceptionHandler.handleException(exception, exchange))
.assertNext(response -> {
assertEquals(HttpStatus.BAD_REQUEST, response.statusCode());
})
.verifyComplete();
}
@Test
@DisplayName("处理通用 Exception")
void testHandleGenericException() {
Exception exception = new Exception("Unexpected error");
StepVerifier.create(globalExceptionHandler.handleException(exception, exchange))
.assertNext(response -> {
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.statusCode());
})
.verifyComplete();
}
@Test
@DisplayName("处理未知 Throwable(非 Exception")
void testHandleUnknownException() {
Throwable throwable = new Throwable("Unknown error");
StepVerifier.create(globalExceptionHandler.handleException(throwable, exchange))
.assertNext(response -> {
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.statusCode());
})
.verifyComplete();
}
@Test
@DisplayName("处理 RuntimeException(继承自 Exception")
void testHandleRuntimeException() {
RuntimeException exception = new RuntimeException("Runtime error");
StepVerifier.create(globalExceptionHandler.handleException(exception, exchange))
.assertNext(response -> {
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.statusCode());
})
.verifyComplete();
}
@Test
@DisplayName("处理 NullPointerException(继承自 RuntimeException")
void testHandleNullPointerException() {
NullPointerException exception = new NullPointerException("Null pointer");
StepVerifier.create(globalExceptionHandler.handleException(exception, exchange))
.assertNext(response -> {
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.statusCode());
})
.verifyComplete();
}
@Test
@DisplayName("处理 Error(非 Exception")
void testHandleError() {
Error error = new Error("Fatal error");
StepVerifier.create(globalExceptionHandler.handleException(error, exchange))
.assertNext(response -> {
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.statusCode());
})
.verifyComplete();
}
}
@@ -0,0 +1,239 @@
package io.destiny.client.core.service;
import io.destiny.client.core.domain.ClientLoginLog;
import io.destiny.client.core.domain.ClientUser;
import io.destiny.client.core.domain.query.ClientUserLogin;
import io.destiny.client.core.repository.IClientLoginLogRepository;
import io.destiny.client.core.repository.IClientUserRepository;
import io.destiny.client.dto.ClientLoginResponse;
import io.destiny.client.security.JwtTokenProvider;
import io.destiny.common.exception.ApplicationException;
import io.destiny.common.primitive.EmailAddress;
import io.destiny.common.primitive.IpAddress;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class ClientUserAuthServiceTest {
@Mock
private IClientUserRepository clientUserRepository;
@Mock
private IClientLoginLogRepository clientLoginLogRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private JwtTokenProvider jwtTokenProvider;
@InjectMocks
private ClientUserAuthService clientUserAuthService;
private ClientUser testUser;
private ClientUserLogin testLogin;
@BeforeEach
void setUp() {
testUser = new ClientUser();
testUser.setId(1L);
testUser.setUsername("testuser");
testUser.setPassword("encodedPassword");
testUser.setEmail(EmailAddress.of("test@example.com"));
testLogin = new ClientUserLogin();
testLogin.setUsername("testuser");
testLogin.setPassword("rawPassword");
testLogin.setIpAddress(IpAddress.of("127.0.0.1"));
}
@Test
void testRegister_Success() {
ClientUser newUser = new ClientUser();
newUser.setUsername("newuser");
newUser.setPassword("rawPassword");
newUser.setEmail(EmailAddress.of("new@example.com"));
when(passwordEncoder.encode(anyString())).thenReturn("encodedPassword");
when(clientUserRepository.save(any(ClientUser.class))).thenReturn(Mono.just(testUser));
Mono<ClientUser> result = clientUserAuthService.register(newUser);
assertNotNull(result);
ClientUser registeredUser = result.block();
assertEquals("testuser", registeredUser.getUsername());
assertEquals("encodedPassword", registeredUser.getPassword());
verify(passwordEncoder, times(1)).encode(anyString());
verify(clientUserRepository, times(1)).save(any(ClientUser.class));
}
@Test
void testLogin_Success() {
when(clientUserRepository.findByUsername(anyString())).thenReturn(Mono.just(testUser));
when(passwordEncoder.matches(anyString(), anyString())).thenReturn(true);
when(clientLoginLogRepository.save(any(ClientLoginLog.class))).thenReturn(Mono.just(new ClientLoginLog()));
when(jwtTokenProvider.generateToken(anyLong(), anyString())).thenReturn("test-token");
Mono<ClientLoginResponse> result = clientUserAuthService.login(testLogin);
assertNotNull(result);
ClientLoginResponse response = result.block();
assertNotNull(response);
assertEquals("test-token", response.getToken());
verify(clientUserRepository, times(1)).findByUsername(anyString());
verify(passwordEncoder, times(1)).matches(anyString(), anyString());
verify(clientLoginLogRepository, times(1)).save(any(ClientLoginLog.class));
verify(jwtTokenProvider, times(1)).generateToken(anyLong(), anyString());
}
@Test
void testLogin_InvalidPassword() {
when(clientUserRepository.findByUsername(anyString())).thenReturn(Mono.just(testUser));
when(passwordEncoder.matches(anyString(), anyString())).thenReturn(false);
Mono<ClientLoginResponse> result = clientUserAuthService.login(testLogin);
assertThrows(ApplicationException.class, () -> result.block());
verify(clientUserRepository, times(1)).findByUsername(anyString());
verify(passwordEncoder, times(1)).matches(anyString(), anyString());
verify(clientLoginLogRepository, never()).save(any(ClientLoginLog.class));
verify(jwtTokenProvider, never()).generateToken(anyLong(), anyString());
}
@Test
void testLogin_UserNotFound() {
when(clientUserRepository.findByUsername(anyString())).thenReturn(Mono.empty());
Mono<ClientLoginResponse> result = clientUserAuthService.login(testLogin);
assertThrows(ApplicationException.class, () -> result.block());
verify(clientUserRepository, times(1)).findByUsername(anyString());
verify(passwordEncoder, never()).matches(anyString(), anyString());
}
@Test
void testRefreshLoginToken_Success() {
String oldToken = "old-test-token";
when(jwtTokenProvider.getUserIdFromToken(anyString())).thenReturn(1L);
when(clientUserRepository.findById(anyLong())).thenReturn(Mono.just(testUser));
when(jwtTokenProvider.generateToken(anyLong(), anyString())).thenReturn("new-test-token");
Mono<ClientLoginResponse> result = clientUserAuthService.refreshLoginToken(oldToken);
assertNotNull(result);
ClientLoginResponse response = result.block();
assertNotNull(response);
assertEquals("new-test-token", response.getToken());
verify(jwtTokenProvider, times(1)).getUserIdFromToken(anyString());
verify(clientUserRepository, times(1)).findById(anyLong());
verify(jwtTokenProvider, times(1)).generateToken(anyLong(), anyString());
}
@Test
void testRefreshLoginToken_InvalidToken() {
String oldToken = "invalid-token";
when(jwtTokenProvider.getUserIdFromToken(anyString())).thenReturn(null);
Mono<ClientLoginResponse> result = clientUserAuthService.refreshLoginToken(oldToken);
assertThrows(ApplicationException.class, () -> result.block());
verify(jwtTokenProvider, times(1)).getUserIdFromToken(anyString());
verify(clientUserRepository, never()).findById(anyLong());
}
@Test
void testRefreshLoginToken_UserNotFound() {
String oldToken = "old-test-token";
when(jwtTokenProvider.getUserIdFromToken(anyString())).thenReturn(1L);
when(clientUserRepository.findById(anyLong())).thenReturn(Mono.empty());
Mono<ClientLoginResponse> result = clientUserAuthService.refreshLoginToken(oldToken);
assertThrows(ApplicationException.class, () -> result.block());
verify(jwtTokenProvider, times(1)).getUserIdFromToken(anyString());
verify(clientUserRepository, times(1)).findById(anyLong());
}
@Test
void testLogout_Success() {
Long userId = 1L;
ClientLoginLog loginLog = new ClientLoginLog();
loginLog.setLogoutTime(null);
when(clientLoginLogRepository.findByClientUserId(anyLong())).thenReturn(Flux.just(loginLog));
when(clientLoginLogRepository.save(any(ClientLoginLog.class))).thenReturn(Mono.just(loginLog));
Mono<Void> result = clientUserAuthService.logout(userId);
assertNotNull(result);
result.block();
verify(clientLoginLogRepository, times(1)).findByClientUserId(anyLong());
verify(clientLoginLogRepository, times(1)).save(any(ClientLoginLog.class));
}
@Test
void testLogout_NoActiveSession() {
Long userId = 1L;
when(clientLoginLogRepository.findByClientUserId(anyLong())).thenReturn(Flux.empty());
Mono<Void> result = clientUserAuthService.logout(userId);
assertNotNull(result);
result.block();
verify(clientLoginLogRepository, times(1)).findByClientUserId(anyLong());
verify(clientLoginLogRepository, never()).save(any(ClientLoginLog.class));
}
@Test
void testCheckLoginStatus_Active() {
Long userId = 1L;
ClientLoginLog loginLog = new ClientLoginLog();
loginLog.setLogoutTime(null);
when(clientLoginLogRepository.findByClientUserId(anyLong())).thenReturn(Flux.just(loginLog));
Mono<Boolean> result = clientUserAuthService.checkLoginStatus(userId);
assertNotNull(result);
Boolean isActive = result.block();
assertTrue(isActive);
verify(clientLoginLogRepository, times(1)).findByClientUserId(anyLong());
}
@Test
void testCheckLoginStatus_Inactive() {
Long userId = 1L;
when(clientLoginLogRepository.findByClientUserId(anyLong())).thenReturn(Flux.empty());
Mono<Boolean> result = clientUserAuthService.checkLoginStatus(userId);
assertNotNull(result);
Boolean isActive = result.block();
assertFalse(isActive);
verify(clientLoginLogRepository, times(1)).findByClientUserId(anyLong());
}
}
@@ -0,0 +1,189 @@
package io.destiny.client.core.service;
import io.destiny.client.core.domain.ClientUser;
import io.destiny.client.core.domain.query.ClientUserQuery;
import io.destiny.client.core.repository.IClientUserRepository;
import io.destiny.common.exception.ApplicationException;
import io.destiny.common.primitive.EmailAddress;
import io.destiny.common.request.PageQuery;
import io.destiny.common.response.PageModel;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class ClientUserServiceTest {
@Mock
private IClientUserRepository clientUserRepository;
@InjectMocks
private ClientUserService clientUserService;
private ClientUser testUser;
@BeforeEach
void setUp() {
testUser = new ClientUser();
testUser.setId(1L);
testUser.setUsername("testuser");
testUser.setPassword("password");
testUser.setEmail(EmailAddress.of("test@example.com"));
testUser.setCreatedAt(LocalDateTime.now());
}
@Test
void testSave_Success() {
when(clientUserRepository.save(any(ClientUser.class))).thenReturn(Mono.just(testUser));
Mono<ClientUser> result = clientUserService.save(testUser);
assertNotNull(result);
ClientUser savedUser = result.block();
assertEquals(1L, savedUser.getId());
assertEquals("testuser", savedUser.getUsername());
verify(clientUserRepository, times(1)).save(any(ClientUser.class));
}
@Test
void testUpdate_Success() {
ClientUser updateUser = new ClientUser();
updateUser.setId(1L);
updateUser.setUsername("updateduser");
updateUser.setPassword("newpassword");
when(clientUserRepository.findById(anyLong())).thenReturn(Mono.just(testUser));
when(clientUserRepository.save(any(ClientUser.class))).thenReturn(Mono.just(updateUser));
Mono<ClientUser> result = clientUserService.update(updateUser);
assertNotNull(result);
ClientUser updatedUser = result.block();
assertEquals("updateduser", updatedUser.getUsername());
verify(clientUserRepository, times(1)).findById(anyLong());
verify(clientUserRepository, times(1)).save(any(ClientUser.class));
}
@Test
void testUpdate_UserNotFound() {
ClientUser updateUser = new ClientUser();
updateUser.setId(999L);
updateUser.setUsername("updateduser");
when(clientUserRepository.findById(anyLong())).thenReturn(Mono.empty());
Mono<ClientUser> result = clientUserService.update(updateUser);
assertThrows(ApplicationException.class, () -> result.block());
verify(clientUserRepository, times(1)).findById(anyLong());
verify(clientUserRepository, never()).save(any(ClientUser.class));
}
@Test
void testDeleteById_Success() {
when(clientUserRepository.findById(anyLong())).thenReturn(Mono.just(testUser));
when(clientUserRepository.save(any(ClientUser.class))).thenReturn(Mono.just(testUser));
Mono<Void> result = clientUserService.deleteById(1L);
assertNotNull(result);
result.block();
assertNotNull(testUser.getDeletedAt());
verify(clientUserRepository, times(1)).findById(anyLong());
verify(clientUserRepository, times(1)).save(any(ClientUser.class));
}
@Test
void testDeleteById_UserNotFound() {
when(clientUserRepository.findById(anyLong())).thenReturn(Mono.empty());
Mono<Void> result = clientUserService.deleteById(999L);
assertThrows(ApplicationException.class, () -> result.block());
verify(clientUserRepository, times(1)).findById(anyLong());
verify(clientUserRepository, never()).save(any(ClientUser.class));
}
@Test
void testFindById_Success() {
when(clientUserRepository.findById(anyLong())).thenReturn(Mono.just(testUser));
Mono<ClientUser> result = clientUserService.findById(1L);
assertNotNull(result);
ClientUser foundUser = result.block();
assertEquals(1L, foundUser.getId());
assertEquals("testuser", foundUser.getUsername());
verify(clientUserRepository, times(1)).findById(anyLong());
}
@Test
void testFindById_NotFound() {
when(clientUserRepository.findById(anyLong())).thenReturn(Mono.empty());
Mono<ClientUser> result = clientUserService.findById(999L);
assertNotNull(result);
ClientUser foundUser = result.block();
assertNull(foundUser);
verify(clientUserRepository, times(1)).findById(anyLong());
}
@Test
void testFindByPage_Success() {
ClientUserQuery query = new ClientUserQuery();
PageQuery pageQuery = new PageQuery(0, 10);
PageModel<ClientUser> pageModel = new PageModel<>(1L, Collections.singletonList(testUser));
when(clientUserRepository.findByQueryWithPagination(any(ClientUserQuery.class), any(PageQuery.class)))
.thenReturn(Mono.just(pageModel));
Mono<PageModel<ClientUser>> result = clientUserService.findByPage(query, pageQuery);
assertNotNull(result);
PageModel<ClientUser> resultPage = result.block();
assertEquals(1, resultPage.getData().size());
assertEquals(1L, resultPage.getCount());
verify(clientUserRepository, times(1)).findByQueryWithPagination(any(ClientUserQuery.class), any(PageQuery.class));
}
@Test
void testFindByPage_EmptyResult() {
ClientUserQuery query = new ClientUserQuery();
PageQuery pageQuery = new PageQuery(0, 10);
PageModel<ClientUser> pageModel = new PageModel<>(0L, Collections.emptyList());
when(clientUserRepository.findByQueryWithPagination(any(ClientUserQuery.class), any(PageQuery.class)))
.thenReturn(Mono.just(pageModel));
Mono<PageModel<ClientUser>> result = clientUserService.findByPage(query, pageQuery);
assertNotNull(result);
PageModel<ClientUser> resultPage = result.block();
assertTrue(resultPage.getData().isEmpty());
assertEquals(0L, resultPage.getCount());
verify(clientUserRepository, times(1)).findByQueryWithPagination(any(ClientUserQuery.class), any(PageQuery.class));
}
}
@@ -0,0 +1,344 @@
package io.destiny.client.core.service;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.dysmsapi20170525.models.SendSmsResponseBody;
import com.github.benmanes.caffeine.cache.Cache;
import io.destiny.client.config.AliyunSmsConfig;
import io.destiny.client.core.domain.SmsVerificationCode;
import io.destiny.client.core.enums.SmsCodeType;
import io.destiny.client.core.exception.ClientUserAuthExceptionCode;
import io.destiny.client.core.repository.ISmsVerificationCodeRepository;
import io.destiny.common.exception.ApplicationException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("SmsService 测试")
class SmsServiceTest {
@Mock
private AliyunSmsConfig smsConfig;
@Mock
private ISmsVerificationCodeRepository smsVerificationCodeRepository;
@Mock
private Client smsClient;
private SmsService smsService;
@BeforeEach
void setUp() {
lenient().when(smsConfig.getSignName()).thenReturn("TestSign");
smsService = new SmsService(smsConfig, smsVerificationCodeRepository, smsClient);
}
@Test
@DisplayName("发送验证码 - 手机号为 null")
void testSendVerificationCodeWithNullPhone() {
StepVerifier.create(smsService.sendVerificationCode(null, SmsCodeType.LOGIN))
.expectErrorMatches(throwable -> throwable instanceof ApplicationException &&
((ApplicationException) throwable)
.getExceptionCode() == ClientUserAuthExceptionCode.CLIENT_SMS_CODE_INVALID)
.verify();
}
@Test
@DisplayName("发送验证码 - 手机号为空字符串")
void testSendVerificationCodeWithEmptyPhone() {
StepVerifier.create(smsService.sendVerificationCode("", SmsCodeType.LOGIN))
.expectErrorMatches(throwable -> throwable instanceof ApplicationException &&
((ApplicationException) throwable)
.getExceptionCode() == ClientUserAuthExceptionCode.CLIENT_SMS_CODE_INVALID)
.verify();
}
@Test
@DisplayName("发送验证码 - SmsCodeType 为 null")
void testSendVerificationCodeWithNullType() {
StepVerifier.create(smsService.sendVerificationCode("13800138000", null))
.expectErrorMatches(throwable -> throwable instanceof ApplicationException &&
((ApplicationException) throwable)
.getExceptionCode() == ClientUserAuthExceptionCode.CLIENT_SMS_CODE_SEND_EXCEEDED)
.verify();
}
@Test
@DisplayName("发送验证码 - 新手机号成功发送")
void testSendVerificationCodeSuccess() throws Exception {
String phone = "13800138000";
SmsCodeType type = SmsCodeType.LOGIN;
SendSmsResponseBody responseBody = new SendSmsResponseBody();
responseBody.setCode("OK");
responseBody.setRequestId("test-request-id");
SendSmsResponse response = new SendSmsResponse();
response.setBody(responseBody);
when(smsClient.sendSms(any(SendSmsRequest.class))).thenReturn(response);
SmsVerificationCode savedCode = new SmsVerificationCode();
when(smsVerificationCodeRepository.save(any(SmsVerificationCode.class)))
.thenReturn(Mono.just(savedCode));
StepVerifier.create(smsService.sendVerificationCode(phone, type))
.assertNext(code -> {
assertNotNull(code);
assertEquals(6, code.length());
assertTrue(code.chars().allMatch(Character::isDigit));
})
.verifyComplete();
ArgumentCaptor<SmsVerificationCode> codeCaptor = ArgumentCaptor.forClass(SmsVerificationCode.class);
verify(smsVerificationCodeRepository).save(codeCaptor.capture());
SmsVerificationCode capturedCode = codeCaptor.getValue();
assertEquals(phone, capturedCode.getPhone());
assertEquals(type.getCode(), capturedCode.getType());
assertNotNull(capturedCode.getExpireTime());
assertTrue(capturedCode.getExpireTime().isAfter(LocalDateTime.now()));
}
@Test
@DisplayName("发送验证码 - 分钟速率限制已达到且未过期")
void testSendVerificationCodeMinuteRateLimitExceeded() throws Exception {
String phone = "13800138000";
SmsCodeType type = SmsCodeType.LOGIN;
SendSmsResponseBody responseBody = new SendSmsResponseBody();
responseBody.setCode("OK");
responseBody.setRequestId("test-request-id");
SendSmsResponse response = new SendSmsResponse();
response.setBody(responseBody);
when(smsClient.sendSms(any(SendSmsRequest.class))).thenReturn(response);
when(smsVerificationCodeRepository.save(any(SmsVerificationCode.class)))
.thenReturn(Mono.just(new SmsVerificationCode()));
StepVerifier.create(smsService.sendVerificationCode(phone, type))
.assertNext(code -> assertNotNull(code))
.verifyComplete();
StepVerifier.create(smsService.sendVerificationCode(phone, type))
.expectErrorMatches(throwable -> throwable instanceof ApplicationException &&
((ApplicationException) throwable)
.getExceptionCode() == ClientUserAuthExceptionCode.CLIENT_SMS_CODE_SEND_TOO_FAST)
.verify();
}
@Test
@DisplayName("发送验证码 - 分钟速率限制已达到但已过期")
void testSendVerificationCodeMinuteRateLimitExpired() throws Exception {
String phone = "13800138000";
SmsCodeType type = SmsCodeType.LOGIN;
SendSmsResponseBody responseBody = new SendSmsResponseBody();
responseBody.setCode("OK");
responseBody.setRequestId("test-request-id");
SendSmsResponse response = new SendSmsResponse();
response.setBody(responseBody);
when(smsClient.sendSms(any(SendSmsRequest.class))).thenReturn(response);
when(smsVerificationCodeRepository.save(any(SmsVerificationCode.class)))
.thenReturn(Mono.just(new SmsVerificationCode()));
StepVerifier.create(smsService.sendVerificationCode(phone, type))
.assertNext(code -> assertNotNull(code))
.verifyComplete();
modifySendRecordExpireTime(phone, LocalDateTime.now().minusMinutes(1));
StepVerifier.create(smsService.sendVerificationCode(phone, type))
.assertNext(code -> assertNotNull(code))
.verifyComplete();
}
@Test
@DisplayName("发送验证码 - 日速率限制已达到且未过期")
void testSendVerificationCodeDayRateLimitExceeded() throws Exception {
String phone = "13800138000";
SmsCodeType type = SmsCodeType.LOGIN;
SendSmsResponseBody responseBody = new SendSmsResponseBody();
responseBody.setCode("OK");
responseBody.setRequestId("test-request-id");
SendSmsResponse response = new SendSmsResponse();
response.setBody(responseBody);
when(smsClient.sendSms(any(SendSmsRequest.class))).thenReturn(response);
when(smsVerificationCodeRepository.save(any(SmsVerificationCode.class)))
.thenReturn(Mono.just(new SmsVerificationCode()));
for (int i = 0; i < 10; i++) {
StepVerifier.create(smsService.sendVerificationCode(phone, type))
.assertNext(code -> assertNotNull(code))
.verifyComplete();
modifySendRecordExpireTime(phone, LocalDateTime.now().minusMinutes(1));
}
StepVerifier.create(smsService.sendVerificationCode(phone, type))
.expectErrorMatches(throwable -> throwable instanceof ApplicationException &&
((ApplicationException) throwable)
.getExceptionCode() == ClientUserAuthExceptionCode.CLIENT_SMS_CODE_SEND_EXCEEDED)
.verify();
}
@Test
@DisplayName("发送验证码 - 日速率限制已达到但已过期")
void testSendVerificationCodeDayRateLimitExpired() throws Exception {
String phone = "13800138000";
SmsCodeType type = SmsCodeType.LOGIN;
SendSmsResponseBody responseBody = new SendSmsResponseBody();
responseBody.setCode("OK");
responseBody.setRequestId("test-request-id");
SendSmsResponse response = new SendSmsResponse();
response.setBody(responseBody);
when(smsClient.sendSms(any(SendSmsRequest.class))).thenReturn(response);
when(smsVerificationCodeRepository.save(any(SmsVerificationCode.class)))
.thenReturn(Mono.just(new SmsVerificationCode()));
for (int i = 0; i < 10; i++) {
StepVerifier.create(smsService.sendVerificationCode(phone, type))
.assertNext(code -> assertNotNull(code))
.verifyComplete();
modifySendRecordExpireTime(phone, LocalDateTime.now().minusMinutes(1));
}
modifySendRecordDayExpireTime(phone, LocalDateTime.now().minusDays(1));
StepVerifier.create(smsService.sendVerificationCode(phone, type))
.assertNext(code -> assertNotNull(code))
.verifyComplete();
}
@Test
@DisplayName("发送验证码 - 阿里云短信发送失败")
void testSendVerificationCodeAliyunSendFailed() throws Exception {
String phone = "13800138000";
SmsCodeType type = SmsCodeType.LOGIN;
SendSmsResponseBody responseBody = new SendSmsResponseBody();
responseBody.setCode("FAILED");
responseBody.setMessage("Invalid phone number");
SendSmsResponse response = new SendSmsResponse();
response.setBody(responseBody);
when(smsClient.sendSms(any(SendSmsRequest.class))).thenReturn(response);
StepVerifier.create(smsService.sendVerificationCode(phone, type))
.expectErrorMatches(throwable -> throwable instanceof ApplicationException &&
((ApplicationException) throwable)
.getExceptionCode() == ClientUserAuthExceptionCode.CLIENT_SMS_CODE_SEND_EXCEEDED)
.verify();
}
@Test
@DisplayName("发送验证码 - 短信签名为 null")
void testSendVerificationCodeWithNullSignName() throws Exception {
String phone = "13800138000";
SmsCodeType type = SmsCodeType.LOGIN;
when(smsConfig.getSignName()).thenReturn(null);
smsService = new SmsService(smsConfig, smsVerificationCodeRepository, smsClient);
StepVerifier.create(smsService.sendVerificationCode(phone, type))
.expectErrorMatches(throwable -> throwable instanceof ApplicationException &&
((ApplicationException) throwable)
.getExceptionCode() == ClientUserAuthExceptionCode.CLIENT_SMS_CODE_SEND_EXCEEDED)
.verify();
}
@Test
@DisplayName("发送验证码 - 不同的验证码类型")
void testSendVerificationCodeWithDifferentTypes() throws Exception {
SendSmsResponseBody responseBody = new SendSmsResponseBody();
responseBody.setCode("OK");
responseBody.setRequestId("test-request-id");
SendSmsResponse response = new SendSmsResponse();
response.setBody(responseBody);
when(smsClient.sendSms(any(SendSmsRequest.class))).thenReturn(response);
SmsVerificationCode savedCode = new SmsVerificationCode();
when(smsVerificationCodeRepository.save(any(SmsVerificationCode.class)))
.thenReturn(Mono.just(savedCode));
SmsCodeType[] types = { SmsCodeType.LOGIN, SmsCodeType.REGISTER, SmsCodeType.BIND,
SmsCodeType.RESET_PASSWORD };
for (SmsCodeType type : types) {
String phone = "1380013800" + type.getCode();
StepVerifier.create(smsService.sendVerificationCode(phone, type))
.assertNext(code -> assertNotNull(code))
.verifyComplete();
ArgumentCaptor<SmsVerificationCode> codeCaptor = ArgumentCaptor
.forClass(SmsVerificationCode.class);
verify(smsVerificationCodeRepository, atLeastOnce()).save(codeCaptor.capture());
SmsVerificationCode capturedCode = codeCaptor.getValue();
assertEquals(type.getCode(), capturedCode.getType());
}
}
private void modifySendRecordExpireTime(String phone, LocalDateTime newExpireTime) {
try {
Field cacheField = SmsService.class.getDeclaredField("sendRecords");
cacheField.setAccessible(true);
@SuppressWarnings("unchecked")
Cache<String, Object> cache = (Cache<String, Object>) cacheField.get(smsService);
Object sendRecord = cache.getIfPresent(phone);
if (sendRecord != null) {
Field minuteExpireTimeField = sendRecord.getClass()
.getDeclaredField("minuteExpireTime");
minuteExpireTimeField.setAccessible(true);
minuteExpireTimeField.set(sendRecord, newExpireTime);
}
} catch (Exception e) {
throw new RuntimeException("Failed to modify send record expire time", e);
}
}
private void modifySendRecordDayExpireTime(String phone, LocalDateTime newExpireTime) {
try {
Field cacheField = SmsService.class.getDeclaredField("sendRecords");
cacheField.setAccessible(true);
@SuppressWarnings("unchecked")
Cache<String, Object> cache = (Cache<String, Object>) cacheField.get(smsService);
Object sendRecord = cache.getIfPresent(phone);
if (sendRecord != null) {
Field dayExpireTimeField = sendRecord.getClass().getDeclaredField("dayExpireTime");
dayExpireTimeField.setAccessible(true);
dayExpireTimeField.set(sendRecord, newExpireTime);
}
} catch (Exception e) {
throw new RuntimeException("Failed to modify send record day expire time", e);
}
}
}
@@ -0,0 +1,324 @@
package io.destiny.client.core.strategy;
import io.destiny.client.client.DouyinMiniProgramClient;
import io.destiny.client.core.domain.ClientUser;
import io.destiny.client.core.domain.UserBinding;
import io.destiny.client.core.repository.IClientUserRepository;
import io.destiny.client.core.repository.IUserBindingRepository;
import io.destiny.client.core.service.ClientUserAuthService;
import io.destiny.client.dto.DouyinLoginRequest;
import io.destiny.client.dto.LoginRequest;
import io.destiny.client.dto.WechatAuthResponse;
import io.destiny.client.security.JwtTokenProvider;
import io.destiny.common.exception.ApplicationException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static io.destiny.client.core.exception.ClientUserAuthExceptionCode.CLIENT_INVALID_DOUYIN_CODE;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class DouyinLoginStrategyTest {
@Mock
private ClientUserAuthService clientUserAuthService;
@Mock
private JwtTokenProvider jwtTokenProvider;
@Mock
private DouyinMiniProgramClient douyinClient;
@Mock
private IClientUserRepository clientUserRepository;
@Mock
private IUserBindingRepository userBindingRepository;
@InjectMocks
private DouyinLoginStrategy douyinLoginStrategy;
@BeforeEach
void setUp() {
}
@Test
void testGetLoginType() {
assertEquals("douyin", douyinLoginStrategy.getLoginType());
}
@Test
void testValidateRequestWithValidData() {
LoginRequest request = new LoginRequest();
DouyinLoginRequest douyinRequest = new DouyinLoginRequest();
douyinRequest.setCode("valid_code");
request.setData(douyinRequest);
StepVerifier.create(douyinLoginStrategy.validateRequest(request))
.expectNext(request)
.verifyComplete();
}
@Test
void testValidateRequestWithNullCode() {
LoginRequest request = new LoginRequest();
DouyinLoginRequest douyinRequest = new DouyinLoginRequest();
douyinRequest.setCode(null);
request.setData(douyinRequest);
StepVerifier.create(douyinLoginStrategy.validateRequest(request))
.expectErrorMatches(error -> error instanceof ApplicationException &&
((ApplicationException) error).getExceptionCode().equals(CLIENT_INVALID_DOUYIN_CODE))
.verify();
}
@Test
void testValidateRequestWithEmptyCode() {
LoginRequest request = new LoginRequest();
DouyinLoginRequest douyinRequest = new DouyinLoginRequest();
douyinRequest.setCode("");
request.setData(douyinRequest);
StepVerifier.create(douyinLoginStrategy.validateRequest(request))
.expectErrorMatches(error -> error instanceof ApplicationException &&
((ApplicationException) error).getExceptionCode().equals(CLIENT_INVALID_DOUYIN_CODE))
.verify();
}
@Test
void testValidateRequestWithInvalidDataType() {
LoginRequest request = new LoginRequest();
request.setData("invalid_data");
StepVerifier.create(douyinLoginStrategy.validateRequest(request))
.expectErrorMatches(error -> error instanceof ApplicationException &&
((ApplicationException) error).getExceptionCode().equals(CLIENT_INVALID_DOUYIN_CODE))
.verify();
}
@Test
void testLoginSuccessWithExistingUserByOpenId() {
when(jwtTokenProvider.generateToken(anyLong(), anyString())).thenReturn("test-token");
LoginRequest request = new LoginRequest();
request.setLoginType("douyin");
request.setIpAddress("192.168.1.1");
DouyinLoginRequest douyinRequest = new DouyinLoginRequest();
douyinRequest.setCode("valid_code");
request.setData(douyinRequest);
WechatAuthResponse authResponse = new WechatAuthResponse();
authResponse.setOpenId("open_id_123");
authResponse.setUnionId("union_id_456");
authResponse.setSessionKey("session_key");
ClientUser user = new ClientUser();
user.setId(1L);
user.setUsername("testuser");
user.setDouyinOpenId("open_id_123");
when(douyinClient.auth("valid_code")).thenReturn(Mono.just(authResponse));
when(clientUserRepository.findByDouyinOpenId("open_id_123")).thenReturn(Mono.just(user));
StepVerifier.create(douyinLoginStrategy.login(request))
.expectNextMatches(result -> result.getUserId().equals(1L) &&
result.getUsername().equals("testuser") &&
result.getPlatform().equals("douyin") &&
result.getToken().equals("test-token"))
.verifyComplete();
verify(clientUserRepository, never()).findByDouyinUnionId(anyString());
verify(userBindingRepository, never()).findByPlatformAndUnionId(anyString(), anyString());
verify(clientUserRepository, never()).save(any(ClientUser.class));
}
@Test
void testLoginSuccessWithExistingUserByUnionId() {
when(jwtTokenProvider.generateToken(anyLong(), anyString())).thenReturn("test-token");
LoginRequest request = new LoginRequest();
request.setLoginType("douyin");
request.setIpAddress("192.168.1.1");
DouyinLoginRequest douyinRequest = new DouyinLoginRequest();
douyinRequest.setCode("valid_code");
request.setData(douyinRequest);
WechatAuthResponse authResponse = new WechatAuthResponse();
authResponse.setOpenId("open_id_123");
authResponse.setUnionId("union_id_456");
authResponse.setSessionKey("session_key");
ClientUser user = new ClientUser();
user.setId(1L);
user.setUsername("testuser");
user.setDouyinUnionId("union_id_456");
when(douyinClient.auth("valid_code")).thenReturn(Mono.just(authResponse));
when(clientUserRepository.findByDouyinOpenId("open_id_123")).thenReturn(Mono.empty());
when(clientUserRepository.findByDouyinUnionId("union_id_456")).thenReturn(Mono.just(user));
StepVerifier.create(douyinLoginStrategy.login(request))
.expectNextMatches(result -> result.getUserId().equals(1L) &&
result.getUsername().equals("testuser") &&
result.getPlatform().equals("douyin") &&
result.getToken().equals("test-token"))
.verifyComplete();
verify(userBindingRepository, never()).findByPlatformAndUnionId(anyString(), anyString());
verify(clientUserRepository, never()).save(any(ClientUser.class));
}
@Test
void testLoginSuccessWithExistingUserByBinding() {
when(jwtTokenProvider.generateToken(anyLong(), anyString())).thenReturn("test-token");
LoginRequest request = new LoginRequest();
request.setLoginType("douyin");
request.setIpAddress("192.168.1.1");
DouyinLoginRequest douyinRequest = new DouyinLoginRequest();
douyinRequest.setCode("valid_code");
request.setData(douyinRequest);
WechatAuthResponse authResponse = new WechatAuthResponse();
authResponse.setOpenId("open_id_123");
authResponse.setUnionId("union_id_456");
authResponse.setSessionKey("session_key");
ClientUser user = new ClientUser();
user.setId(1L);
user.setUsername("testuser");
UserBinding binding = new UserBinding();
binding.setUserId(1L);
binding.setPlatform("douyin");
binding.setPlatformUnionId("union_id_456");
when(douyinClient.auth("valid_code")).thenReturn(Mono.just(authResponse));
when(clientUserRepository.findByDouyinOpenId("open_id_123")).thenReturn(Mono.empty());
when(clientUserRepository.findByDouyinUnionId("union_id_456")).thenReturn(Mono.empty());
when(userBindingRepository.findByPlatformAndUnionId("douyin", "union_id_456")).thenReturn(Mono.just(binding));
when(clientUserRepository.findById(1L)).thenReturn(Mono.just(user));
StepVerifier.create(douyinLoginStrategy.login(request))
.expectNextMatches(result -> result.getUserId().equals(1L) &&
result.getUsername().equals("testuser") &&
result.getPlatform().equals("douyin") &&
result.getToken().equals("test-token"))
.verifyComplete();
verify(clientUserRepository, never()).save(any(ClientUser.class));
}
@Test
void testLoginSuccessWithNewUser() {
when(jwtTokenProvider.generateToken(anyLong(), anyString())).thenReturn("test-token");
LoginRequest request = new LoginRequest();
request.setLoginType("douyin");
request.setIpAddress("192.168.1.1");
DouyinLoginRequest douyinRequest = new DouyinLoginRequest();
douyinRequest.setCode("valid_code");
request.setData(douyinRequest);
WechatAuthResponse authResponse = new WechatAuthResponse();
authResponse.setOpenId("open_id_123");
authResponse.setUnionId("union_id_456");
authResponse.setSessionKey("session_key");
ClientUser newUser = new ClientUser();
newUser.setId(1L);
newUser.setDouyinOpenId("open_id_123");
newUser.setDouyinUnionId("union_id_456");
newUser.setPhoneVerified(false);
UserBinding binding = new UserBinding();
binding.setId(1L);
binding.setUserId(1L);
binding.setPlatform("douyin");
binding.setPlatformOpenId("open_id_123");
binding.setPlatformUnionId("union_id_456");
when(douyinClient.auth("valid_code")).thenReturn(Mono.just(authResponse));
when(clientUserRepository.findByDouyinOpenId("open_id_123")).thenReturn(Mono.empty());
when(clientUserRepository.findByDouyinUnionId("union_id_456")).thenReturn(Mono.empty());
when(userBindingRepository.findByPlatformAndUnionId("douyin", "union_id_456")).thenReturn(Mono.empty());
when(clientUserRepository.save(any(ClientUser.class))).thenReturn(Mono.just(newUser));
when(userBindingRepository.save(any(UserBinding.class))).thenReturn(Mono.just(binding));
StepVerifier.create(douyinLoginStrategy.login(request))
.expectNextMatches(result -> result.getUserId().equals(1L) &&
result.getPlatform().equals("douyin") &&
result.getToken().equals("test-token"))
.verifyComplete();
verify(clientUserRepository).save(any(ClientUser.class));
verify(userBindingRepository).save(any(UserBinding.class));
}
@Test
void testLoginWithNewUserWithoutUnionId() {
when(jwtTokenProvider.generateToken(anyLong(), anyString())).thenReturn("test-token");
LoginRequest request = new LoginRequest();
request.setLoginType("douyin");
request.setIpAddress("192.168.1.1");
DouyinLoginRequest douyinRequest = new DouyinLoginRequest();
douyinRequest.setCode("valid_code");
request.setData(douyinRequest);
WechatAuthResponse authResponse = new WechatAuthResponse();
authResponse.setOpenId("open_id_123");
authResponse.setSessionKey("session_key");
ClientUser newUser = new ClientUser();
newUser.setId(1L);
newUser.setDouyinOpenId("open_id_123");
newUser.setPhoneVerified(false);
UserBinding binding = new UserBinding();
binding.setId(1L);
binding.setUserId(1L);
binding.setPlatform("douyin");
binding.setPlatformOpenId("open_id_123");
when(douyinClient.auth("valid_code")).thenReturn(Mono.just(authResponse));
when(clientUserRepository.findByDouyinOpenId("open_id_123")).thenReturn(Mono.empty());
when(clientUserRepository.save(any(ClientUser.class))).thenReturn(Mono.just(newUser));
when(userBindingRepository.save(any(UserBinding.class))).thenReturn(Mono.just(binding));
StepVerifier.create(douyinLoginStrategy.login(request))
.expectNextMatches(result -> result.getUserId().equals(1L) &&
result.getPlatform().equals("douyin") &&
result.getToken().equals("test-token"))
.verifyComplete();
verify(clientUserRepository, never()).findByDouyinUnionId(anyString());
verify(userBindingRepository, never()).findByPlatformAndUnionId(anyString(), anyString());
}
@Test
void testLoginWithAuthFailure() {
LoginRequest request = new LoginRequest();
request.setLoginType("douyin");
request.setIpAddress("192.168.1.1");
DouyinLoginRequest douyinRequest = new DouyinLoginRequest();
douyinRequest.setCode("invalid_code");
request.setData(douyinRequest);
when(douyinClient.auth("invalid_code")).thenReturn(Mono.error(new Exception("Auth failed")));
StepVerifier.create(douyinLoginStrategy.login(request))
.expectError(Exception.class)
.verify();
}
}
@@ -0,0 +1,83 @@
package io.destiny.client.core.strategy;
import io.destiny.common.exception.ApplicationException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class LoginStrategyFactoryTest {
@Mock
private WechatLoginStrategy wechatLoginStrategy;
@Mock
private DouyinLoginStrategy douyinLoginStrategy;
@Mock
private SmsLoginStrategy smsLoginStrategy;
private LoginStrategyFactory factory;
@BeforeEach
void setUp() {
when(wechatLoginStrategy.getLoginType()).thenReturn("wechat");
when(douyinLoginStrategy.getLoginType()).thenReturn("douyin");
when(smsLoginStrategy.getLoginType()).thenReturn("sms");
factory = new LoginStrategyFactory(List.of(wechatLoginStrategy, douyinLoginStrategy, smsLoginStrategy));
factory.init();
}
@Test
void testGetStrategy_Wechat() {
LoginStrategy strategy = factory.getStrategy("wechat");
assertNotNull(strategy);
assertEquals(wechatLoginStrategy, strategy);
}
@Test
void testGetStrategy_Douyin() {
LoginStrategy strategy = factory.getStrategy("douyin");
assertNotNull(strategy);
assertEquals(douyinLoginStrategy, strategy);
}
@Test
void testGetStrategy_Sms() {
LoginStrategy strategy = factory.getStrategy("sms");
assertNotNull(strategy);
assertEquals(smsLoginStrategy, strategy);
}
@Test
void testGetStrategy_InvalidType() {
assertThrows(ApplicationException.class, () -> {
factory.getStrategy("invalid");
});
}
@Test
void testGetStrategy_NullType() {
assertThrows(ApplicationException.class, () -> {
factory.getStrategy(null);
});
}
@Test
void testGetStrategy_EmptyType() {
assertThrows(ApplicationException.class, () -> {
factory.getStrategy("");
});
}
}
@@ -0,0 +1,275 @@
package io.destiny.client.core.strategy;
import io.destiny.client.core.exception.ClientUserAuthExceptionCode;
import io.destiny.client.dto.DouyinLoginRequest;
import io.destiny.client.dto.LoginRequest;
import io.destiny.client.dto.LoginResult;
import io.destiny.client.dto.SmsLoginRequest;
import io.destiny.client.dto.WechatLoginRequest;
import io.destiny.common.exception.ApplicationException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class LoginStrategyProxyTest {
@Mock
private LoginStrategyFactory strategyFactory;
@Mock
private LoginStrategy loginStrategy;
@InjectMocks
private LoginStrategyProxy loginStrategyProxy;
@BeforeEach
void setUp() {
when(strategyFactory.getStrategy(any())).thenReturn(loginStrategy);
}
@Test
void testLoginSuccess() {
LoginRequest request = new LoginRequest();
request.setLoginType("wechat");
request.setIpAddress("192.168.1.1");
request.setData(new WechatLoginRequest());
LoginResult result = new LoginResult();
result.setUserId(1L);
result.setPlatform("wechat");
when(loginStrategy.login(any())).thenReturn(Mono.just(result));
StepVerifier.create(loginStrategyProxy.login(request))
.expectNext(result)
.verifyComplete();
verify(loginStrategy).login(request);
}
@Test
void testLoginWithNullIpAddress() {
LoginRequest request = new LoginRequest();
request.setLoginType("wechat");
request.setIpAddress(null);
request.setData(new WechatLoginRequest());
LoginResult result = new LoginResult();
result.setUserId(1L);
result.setPlatform("wechat");
when(loginStrategy.login(any())).thenReturn(Mono.just(result));
StepVerifier.create(loginStrategyProxy.login(request))
.expectNext(result)
.verifyComplete();
}
@Test
void testLoginFailureWithApplicationException() {
LoginRequest request = new LoginRequest();
request.setLoginType("wechat");
request.setIpAddress("192.168.1.1");
request.setData(new WechatLoginRequest());
ApplicationException exception = new ApplicationException(ClientUserAuthExceptionCode.CLIENT_INVALID_USERNAME_OR_PASSWORD);
when(loginStrategy.login(any())).thenReturn(Mono.error(exception));
StepVerifier.create(loginStrategyProxy.login(request))
.expectError(ApplicationException.class)
.verify();
}
@Test
void testLoginFailureWithNonApplicationException() {
LoginRequest request = new LoginRequest();
request.setLoginType("wechat");
request.setIpAddress("192.168.1.1");
request.setData(new WechatLoginRequest());
RuntimeException exception = new RuntimeException("Unexpected error");
when(loginStrategy.login(any())).thenReturn(Mono.error(exception));
StepVerifier.create(loginStrategyProxy.login(request))
.expectErrorMatches(error -> error instanceof ApplicationException &&
((ApplicationException) error).getExceptionCode().equals(ClientUserAuthExceptionCode.CLIENT_LOGIN_FAILED))
.verify();
}
@Test
void testAccountLockAfterMultipleFailedAttempts() {
LoginRequest request = new LoginRequest();
request.setLoginType("wechat");
request.setIpAddress("192.168.1.1");
request.setData(new WechatLoginRequest());
ApplicationException exception = new ApplicationException(ClientUserAuthExceptionCode.CLIENT_INVALID_USERNAME_OR_PASSWORD);
when(loginStrategy.login(any())).thenReturn(Mono.error(exception));
for (int i = 0; i < 5; i++) {
StepVerifier.create(loginStrategyProxy.login(request))
.expectError(ApplicationException.class)
.verify();
}
StepVerifier.create(loginStrategyProxy.login(request))
.expectErrorMatches(error -> error instanceof ApplicationException &&
((ApplicationException) error).getExceptionCode().equals(ClientUserAuthExceptionCode.CLIENT_ACCOUNT_LOCKED))
.verify();
}
@Test
void testLoginWithDouyinRequest() {
LoginRequest request = new LoginRequest();
request.setLoginType("douyin");
request.setIpAddress("192.168.1.1");
DouyinLoginRequest douyinRequest = new DouyinLoginRequest();
douyinRequest.setCode("douyin_code");
request.setData(douyinRequest);
LoginResult result = new LoginResult();
result.setUserId(1L);
result.setPlatform("douyin");
when(loginStrategy.login(any())).thenReturn(Mono.just(result));
StepVerifier.create(loginStrategyProxy.login(request))
.expectNext(result)
.verifyComplete();
}
@Test
void testLoginWithDouyinRequestNullCode() {
LoginRequest request = new LoginRequest();
request.setLoginType("douyin");
request.setIpAddress("192.168.1.1");
DouyinLoginRequest douyinRequest = new DouyinLoginRequest();
douyinRequest.setCode(null);
request.setData(douyinRequest);
LoginResult result = new LoginResult();
result.setUserId(1L);
result.setPlatform("douyin");
when(loginStrategy.login(any())).thenReturn(Mono.just(result));
StepVerifier.create(loginStrategyProxy.login(request))
.expectNext(result)
.verifyComplete();
}
@Test
void testLoginWithSmsRequest() {
LoginRequest request = new LoginRequest();
request.setLoginType("sms");
request.setIpAddress("192.168.1.1");
SmsLoginRequest smsRequest = new SmsLoginRequest();
smsRequest.setPhone("13800138000");
smsRequest.setCode("123456");
request.setData(smsRequest);
LoginResult result = new LoginResult();
result.setUserId(1L);
result.setPlatform("sms");
when(loginStrategy.login(any())).thenReturn(Mono.just(result));
StepVerifier.create(loginStrategyProxy.login(request))
.expectNext(result)
.verifyComplete();
}
@Test
void testLoginWithSmsRequestNullPhone() {
LoginRequest request = new LoginRequest();
request.setLoginType("sms");
request.setIpAddress("192.168.1.1");
SmsLoginRequest smsRequest = new SmsLoginRequest();
smsRequest.setPhone(null);
smsRequest.setCode("123456");
request.setData(smsRequest);
LoginResult result = new LoginResult();
result.setUserId(1L);
result.setPlatform("sms");
when(loginStrategy.login(any())).thenReturn(Mono.just(result));
StepVerifier.create(loginStrategyProxy.login(request))
.expectNext(result)
.verifyComplete();
}
@Test
void testLoginWithNullLoginType() {
LoginRequest request = new LoginRequest();
request.setLoginType(null);
request.setIpAddress("192.168.1.1");
request.setData(new WechatLoginRequest());
LoginResult result = new LoginResult();
result.setUserId(1L);
result.setPlatform("wechat");
when(loginStrategy.login(any())).thenReturn(Mono.just(result));
StepVerifier.create(loginStrategyProxy.login(request))
.expectNext(result)
.verifyComplete();
}
@Test
void testLoginWithNullData() {
LoginRequest request = new LoginRequest();
request.setLoginType("password");
request.setIpAddress("192.168.1.1");
request.setData(null);
LoginResult result = new LoginResult();
result.setUserId(1L);
result.setPlatform("password");
when(loginStrategy.login(any())).thenReturn(Mono.just(result));
StepVerifier.create(loginStrategyProxy.login(request))
.expectNext(result)
.verifyComplete();
}
@Test
void testClearAttemptRecordAfterSuccessfulLogin() {
LoginRequest request = new LoginRequest();
request.setLoginType("wechat");
request.setIpAddress("192.168.1.1");
request.setData(new WechatLoginRequest());
ApplicationException exception = new ApplicationException(ClientUserAuthExceptionCode.CLIENT_INVALID_USERNAME_OR_PASSWORD);
when(loginStrategy.login(any())).thenReturn(Mono.error(exception));
StepVerifier.create(loginStrategyProxy.login(request))
.expectError(ApplicationException.class)
.verify();
LoginResult result = new LoginResult();
result.setUserId(1L);
result.setPlatform("wechat");
when(loginStrategy.login(any())).thenReturn(Mono.just(result));
StepVerifier.create(loginStrategyProxy.login(request))
.expectNext(result)
.verifyComplete();
}
}
@@ -0,0 +1,385 @@
package io.destiny.client.core.strategy;
import io.destiny.client.core.domain.ClientUser;
import io.destiny.client.core.domain.SmsVerificationCode;
import io.destiny.client.core.enums.SmsCodeType;
import io.destiny.client.core.repository.IClientUserRepository;
import io.destiny.client.core.repository.ISmsVerificationCodeRepository;
import io.destiny.client.core.service.ClientUserAuthService;
import io.destiny.client.dto.LoginRequest;
import io.destiny.client.dto.LoginResult;
import io.destiny.client.dto.SmsLoginRequest;
import io.destiny.client.security.JwtTokenProvider;
import io.destiny.common.exception.ApplicationException;
import io.destiny.common.primitive.PhoneNumber;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.LocalDateTime;
import static io.destiny.client.core.exception.ClientUserAuthExceptionCode.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class SmsLoginStrategyTest {
@Mock
private ClientUserAuthService clientUserAuthService;
@Mock
private JwtTokenProvider jwtTokenProvider;
@Mock
private IClientUserRepository clientUserRepository;
@Mock
private ISmsVerificationCodeRepository smsVerificationCodeRepository;
private SmsLoginStrategy strategy;
private static final String TEST_PHONE = "13800138000";
private static final String TEST_CODE = "123456";
private static final Long TEST_USER_ID = 12345L;
private static final String TEST_TOKEN = "test_jwt_token";
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
strategy = new SmsLoginStrategy(clientUserAuthService, jwtTokenProvider, clientUserRepository, smsVerificationCodeRepository);
}
@Test
void testGetLoginType() {
String result = strategy.getLoginType();
assert "sms".equals(result);
}
@Test
void testValidateRequest_Success() {
SmsLoginRequest smsRequest = new SmsLoginRequest();
smsRequest.setPhone(TEST_PHONE);
smsRequest.setCode(TEST_CODE);
LoginRequest request = new LoginRequest();
request.setData(smsRequest);
Mono<LoginRequest> result = strategy.validateRequest(request);
StepVerifier.create(result)
.expectNext(request)
.verifyComplete();
}
@Test
void testValidateRequest_NullPhone() {
SmsLoginRequest smsRequest = new SmsLoginRequest();
smsRequest.setPhone(null);
smsRequest.setCode(TEST_CODE);
LoginRequest request = new LoginRequest();
request.setData(smsRequest);
Mono<LoginRequest> result = strategy.validateRequest(request);
StepVerifier.create(result)
.expectErrorMatches(ex -> ex instanceof ApplicationException &&
((ApplicationException) ex).getExceptionCode().equals(CLIENT_SMS_CODE_INVALID))
.verify();
}
@Test
void testValidateRequest_EmptyPhone() {
SmsLoginRequest smsRequest = new SmsLoginRequest();
smsRequest.setPhone("");
smsRequest.setCode(TEST_CODE);
LoginRequest request = new LoginRequest();
request.setData(smsRequest);
Mono<LoginRequest> result = strategy.validateRequest(request);
StepVerifier.create(result)
.expectErrorMatches(ex -> ex instanceof ApplicationException &&
((ApplicationException) ex).getExceptionCode().equals(CLIENT_SMS_CODE_INVALID))
.verify();
}
@Test
void testValidateRequest_NullCode() {
SmsLoginRequest smsRequest = new SmsLoginRequest();
smsRequest.setPhone(TEST_PHONE);
smsRequest.setCode(null);
LoginRequest request = new LoginRequest();
request.setData(smsRequest);
Mono<LoginRequest> result = strategy.validateRequest(request);
StepVerifier.create(result)
.expectErrorMatches(ex -> ex instanceof ApplicationException &&
((ApplicationException) ex).getExceptionCode().equals(CLIENT_SMS_CODE_INVALID))
.verify();
}
@Test
void testValidateRequest_EmptyCode() {
SmsLoginRequest smsRequest = new SmsLoginRequest();
smsRequest.setPhone(TEST_PHONE);
smsRequest.setCode("");
LoginRequest request = new LoginRequest();
request.setData(smsRequest);
Mono<LoginRequest> result = strategy.validateRequest(request);
StepVerifier.create(result)
.expectErrorMatches(ex -> ex instanceof ApplicationException &&
((ApplicationException) ex).getExceptionCode().equals(CLIENT_SMS_CODE_INVALID))
.verify();
}
@Test
void testValidateRequest_InvalidDataType() {
LoginRequest request = new LoginRequest();
request.setData(new Object());
Mono<LoginRequest> result = strategy.validateRequest(request);
StepVerifier.create(result)
.expectErrorMatches(ex -> ex instanceof ApplicationException &&
((ApplicationException) ex).getExceptionCode().equals(CLIENT_SMS_CODE_INVALID))
.verify();
}
@Test
void testLogin_ExistingUser_Success() {
SmsLoginRequest smsRequest = new SmsLoginRequest();
smsRequest.setPhone(TEST_PHONE);
smsRequest.setCode(TEST_CODE);
LoginRequest request = new LoginRequest();
request.setData(smsRequest);
SmsVerificationCode verificationCode = new SmsVerificationCode();
verificationCode.setCode(TEST_CODE);
verificationCode.setExpireTime(LocalDateTime.now().plusMinutes(5));
verificationCode.setVerified(false);
verificationCode.setVerifyCount(0);
ClientUser existingUser = new ClientUser();
existingUser.setId(TEST_USER_ID);
existingUser.setPhone(PhoneNumber.of(TEST_PHONE));
existingUser.setUsername(TEST_PHONE);
when(smsVerificationCodeRepository.findLatestByPhoneAndType(TEST_PHONE, SmsCodeType.LOGIN.getCode()))
.thenReturn(Mono.just(verificationCode));
when(smsVerificationCodeRepository.markAsVerified(any())).thenReturn(Mono.empty());
when(clientUserRepository.findByPhone(PhoneNumber.of(TEST_PHONE))).thenReturn(Mono.just(existingUser));
when(jwtTokenProvider.generateToken(TEST_USER_ID, "sms")).thenReturn(TEST_TOKEN);
Mono<LoginResult> result = strategy.login(request);
StepVerifier.create(result)
.expectNextMatches(loginResult ->
TEST_USER_ID.equals(loginResult.getUserId()) &&
TEST_PHONE.equals(loginResult.getUsername()) &&
TEST_PHONE.equals(loginResult.getPhone()) &&
"sms".equals(loginResult.getPlatform()) &&
TEST_TOKEN.equals(loginResult.getToken()))
.verifyComplete();
verify(smsVerificationCodeRepository, times(1)).findLatestByPhoneAndType(TEST_PHONE, SmsCodeType.LOGIN.getCode());
verify(smsVerificationCodeRepository, times(1)).markAsVerified(any());
verify(clientUserRepository, times(1)).findByPhone(PhoneNumber.of(TEST_PHONE));
verify(jwtTokenProvider, times(1)).generateToken(TEST_USER_ID, "sms");
}
@Test
void testLogin_NewUser_Success() {
SmsLoginRequest smsRequest = new SmsLoginRequest();
smsRequest.setPhone(TEST_PHONE);
smsRequest.setCode(TEST_CODE);
LoginRequest request = new LoginRequest();
request.setData(smsRequest);
SmsVerificationCode verificationCode = new SmsVerificationCode();
verificationCode.setCode(TEST_CODE);
verificationCode.setExpireTime(LocalDateTime.now().plusMinutes(5));
verificationCode.setVerified(false);
verificationCode.setVerifyCount(0);
ClientUser newUser = new ClientUser();
newUser.setId(TEST_USER_ID);
newUser.setPhone(PhoneNumber.of(TEST_PHONE));
newUser.setPhoneVerified(true);
newUser.setUsername(TEST_PHONE);
when(smsVerificationCodeRepository.findLatestByPhoneAndType(TEST_PHONE, SmsCodeType.LOGIN.getCode()))
.thenReturn(Mono.just(verificationCode));
when(smsVerificationCodeRepository.markAsVerified(any())).thenReturn(Mono.empty());
when(clientUserRepository.findByPhone(PhoneNumber.of(TEST_PHONE))).thenReturn(Mono.empty());
when(clientUserRepository.save(any(ClientUser.class))).thenReturn(Mono.just(newUser));
when(jwtTokenProvider.generateToken(TEST_USER_ID, "sms")).thenReturn(TEST_TOKEN);
Mono<LoginResult> result = strategy.login(request);
StepVerifier.create(result)
.expectNextMatches(loginResult ->
TEST_USER_ID.equals(loginResult.getUserId()) &&
TEST_PHONE.equals(loginResult.getUsername()) &&
TEST_PHONE.equals(loginResult.getPhone()) &&
"sms".equals(loginResult.getPlatform()) &&
TEST_TOKEN.equals(loginResult.getToken()))
.verifyComplete();
verify(smsVerificationCodeRepository, times(1)).findLatestByPhoneAndType(TEST_PHONE, SmsCodeType.LOGIN.getCode());
verify(smsVerificationCodeRepository, times(1)).markAsVerified(any());
verify(clientUserRepository, times(1)).findByPhone(PhoneNumber.of(TEST_PHONE));
verify(clientUserRepository, times(1)).save(any(ClientUser.class));
verify(jwtTokenProvider, times(1)).generateToken(TEST_USER_ID, "sms");
}
@Test
void testLogin_CodeNotFound() {
SmsLoginRequest smsRequest = new SmsLoginRequest();
smsRequest.setPhone(TEST_PHONE);
smsRequest.setCode(TEST_CODE);
LoginRequest request = new LoginRequest();
request.setData(smsRequest);
when(smsVerificationCodeRepository.findLatestByPhoneAndType(TEST_PHONE, SmsCodeType.LOGIN.getCode()))
.thenReturn(Mono.empty());
Mono<LoginResult> result = strategy.login(request);
StepVerifier.create(result)
.expectErrorMatches(ex -> ex instanceof ApplicationException &&
((ApplicationException) ex).getExceptionCode().equals(CLIENT_SMS_CODE_INVALID))
.verify();
verify(smsVerificationCodeRepository, times(1)).findLatestByPhoneAndType(TEST_PHONE, SmsCodeType.LOGIN.getCode());
verify(clientUserRepository, never()).findByPhone(any());
verify(jwtTokenProvider, never()).generateToken(any(), anyString());
}
@Test
void testLogin_CodeExpired() {
SmsLoginRequest smsRequest = new SmsLoginRequest();
smsRequest.setPhone(TEST_PHONE);
smsRequest.setCode(TEST_CODE);
LoginRequest request = new LoginRequest();
request.setData(smsRequest);
SmsVerificationCode verificationCode = new SmsVerificationCode();
verificationCode.setCode(TEST_CODE);
verificationCode.setExpireTime(LocalDateTime.now().minusMinutes(1));
verificationCode.setVerified(false);
verificationCode.setVerifyCount(0);
when(smsVerificationCodeRepository.findLatestByPhoneAndType(TEST_PHONE, SmsCodeType.LOGIN.getCode()))
.thenReturn(Mono.just(verificationCode));
Mono<LoginResult> result = strategy.login(request);
StepVerifier.create(result)
.expectErrorMatches(ex -> ex instanceof ApplicationException &&
((ApplicationException) ex).getExceptionCode().equals(CLIENT_SMS_CODE_EXPIRED))
.verify();
verify(smsVerificationCodeRepository, times(1)).findLatestByPhoneAndType(TEST_PHONE, SmsCodeType.LOGIN.getCode());
verify(clientUserRepository, never()).findByPhone(any());
verify(jwtTokenProvider, never()).generateToken(any(), anyString());
}
@Test
void testLogin_CodeAlreadyUsed() {
SmsLoginRequest smsRequest = new SmsLoginRequest();
smsRequest.setPhone(TEST_PHONE);
smsRequest.setCode(TEST_CODE);
LoginRequest request = new LoginRequest();
request.setData(smsRequest);
SmsVerificationCode verificationCode = new SmsVerificationCode();
verificationCode.setCode(TEST_CODE);
verificationCode.setExpireTime(LocalDateTime.now().plusMinutes(5));
verificationCode.setVerified(true);
verificationCode.setVerifyCount(0);
when(smsVerificationCodeRepository.findLatestByPhoneAndType(TEST_PHONE, SmsCodeType.LOGIN.getCode()))
.thenReturn(Mono.just(verificationCode));
Mono<LoginResult> result = strategy.login(request);
StepVerifier.create(result)
.expectErrorMatches(ex -> ex instanceof ApplicationException &&
((ApplicationException) ex).getExceptionCode().equals(CLIENT_SMS_CODE_USED))
.verify();
verify(smsVerificationCodeRepository, times(1)).findLatestByPhoneAndType(TEST_PHONE, SmsCodeType.LOGIN.getCode());
verify(clientUserRepository, never()).findByPhone(any());
verify(jwtTokenProvider, never()).generateToken(any(), anyString());
}
@Test
void testLogin_VerifyCountExceeded() {
SmsLoginRequest smsRequest = new SmsLoginRequest();
smsRequest.setPhone(TEST_PHONE);
smsRequest.setCode(TEST_CODE);
LoginRequest request = new LoginRequest();
request.setData(smsRequest);
SmsVerificationCode verificationCode = new SmsVerificationCode();
verificationCode.setCode(TEST_CODE);
verificationCode.setExpireTime(LocalDateTime.now().plusMinutes(5));
verificationCode.setVerified(false);
verificationCode.setVerifyCount(5);
when(smsVerificationCodeRepository.findLatestByPhoneAndType(TEST_PHONE, SmsCodeType.LOGIN.getCode()))
.thenReturn(Mono.just(verificationCode));
Mono<LoginResult> result = strategy.login(request);
StepVerifier.create(result)
.expectErrorMatches(ex -> ex instanceof ApplicationException &&
((ApplicationException) ex).getExceptionCode().equals(CLIENT_SMS_CODE_EXCEEDED))
.verify();
verify(smsVerificationCodeRepository, times(1)).findLatestByPhoneAndType(TEST_PHONE, SmsCodeType.LOGIN.getCode());
verify(clientUserRepository, never()).findByPhone(any());
verify(jwtTokenProvider, never()).generateToken(any(), anyString());
}
@Test
void testLogin_WrongCode() {
SmsLoginRequest smsRequest = new SmsLoginRequest();
smsRequest.setPhone(TEST_PHONE);
smsRequest.setCode("999999");
LoginRequest request = new LoginRequest();
request.setData(smsRequest);
SmsVerificationCode verificationCode = new SmsVerificationCode();
verificationCode.setCode(TEST_CODE);
verificationCode.setExpireTime(LocalDateTime.now().plusMinutes(5));
verificationCode.setVerified(false);
verificationCode.setVerifyCount(0);
when(smsVerificationCodeRepository.findLatestByPhoneAndType(TEST_PHONE, SmsCodeType.LOGIN.getCode()))
.thenReturn(Mono.just(verificationCode));
when(smsVerificationCodeRepository.save(any(SmsVerificationCode.class))).thenReturn(Mono.just(verificationCode));
Mono<LoginResult> result = strategy.login(request);
StepVerifier.create(result)
.expectErrorMatches(ex -> ex instanceof ApplicationException &&
((ApplicationException) ex).getExceptionCode().equals(CLIENT_SMS_CODE_INVALID))
.verify();
verify(smsVerificationCodeRepository, times(1)).findLatestByPhoneAndType(TEST_PHONE, SmsCodeType.LOGIN.getCode());
verify(smsVerificationCodeRepository, times(1)).save(any(SmsVerificationCode.class));
verify(clientUserRepository, never()).findByPhone(any());
verify(jwtTokenProvider, never()).generateToken(any(), anyString());
}
}
@@ -0,0 +1,273 @@
package io.destiny.client.core.strategy;
import io.destiny.client.core.domain.ClientLoginLog;
import io.destiny.client.core.domain.ClientUser;
import io.destiny.client.core.repository.IClientLoginLogRepository;
import io.destiny.client.core.repository.IClientUserRepository;
import io.destiny.client.core.service.ClientUserAuthService;
import io.destiny.client.dto.LoginRequest;
import io.destiny.client.security.JwtTokenProvider;
import io.destiny.common.exception.ApplicationException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.HashMap;
import java.util.Map;
import static io.destiny.client.core.exception.ClientUserAuthExceptionCode.CLIENT_INVALID_USERNAME_OR_PASSWORD;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UsernamePasswordLoginStrategyTest {
@Mock
private ClientUserAuthService clientUserAuthService;
@Mock
private JwtTokenProvider jwtTokenProvider;
@Mock
private IClientUserRepository clientUserRepository;
@Mock
private IClientLoginLogRepository clientLoginLogRepository;
@Mock
private PasswordEncoder passwordEncoder;
@InjectMocks
private UsernamePasswordLoginStrategy usernamePasswordLoginStrategy;
@Test
void testGetLoginType() {
assertEquals("password", usernamePasswordLoginStrategy.getLoginType());
}
@Test
void testLoginSuccess() {
when(jwtTokenProvider.generateToken(anyLong(), anyString())).thenReturn("test-token");
LoginRequest request = new LoginRequest();
request.setLoginType("password");
request.setIpAddress("192.168.1.1");
Map<String, Object> data = new HashMap<>();
data.put("username", "testuser");
data.put("password", "password123");
request.setData(data);
ClientUser user = new ClientUser();
user.setId(1L);
user.setUsername("testuser");
user.setPassword("encoded_password");
when(clientUserRepository.findByUsername("testuser")).thenReturn(Mono.just(user));
when(passwordEncoder.matches("password123", "encoded_password")).thenReturn(true);
when(clientLoginLogRepository.save(any(ClientLoginLog.class)))
.thenReturn(Mono.just(new ClientLoginLog()));
StepVerifier.create(usernamePasswordLoginStrategy.login(request))
.expectNextMatches(result -> result.getUserId().equals(1L) &&
result.getUsername().equals("testuser") &&
result.getPlatform().equals("password") &&
result.getToken().equals("test-token"))
.verifyComplete();
verify(clientUserRepository).findByUsername("testuser");
verify(passwordEncoder).matches("password123", "encoded_password");
verify(clientLoginLogRepository).save(any(ClientLoginLog.class));
}
@Test
void testLoginWithNullUsername() {
LoginRequest request = new LoginRequest();
request.setLoginType("password");
request.setIpAddress("192.168.1.1");
Map<String, Object> data = new HashMap<>();
data.put("username", null);
data.put("password", "password123");
request.setData(data);
StepVerifier.create(usernamePasswordLoginStrategy.login(request))
.expectErrorMatches(error -> error instanceof ApplicationException &&
((ApplicationException) error).getExceptionCode()
.equals(CLIENT_INVALID_USERNAME_OR_PASSWORD))
.verify();
verify(clientUserRepository, never()).findByUsername(anyString());
}
@Test
void testLoginWithEmptyUsername() {
LoginRequest request = new LoginRequest();
request.setLoginType("password");
request.setIpAddress("192.168.1.1");
Map<String, Object> data = new HashMap<>();
data.put("username", "");
data.put("password", "password123");
request.setData(data);
StepVerifier.create(usernamePasswordLoginStrategy.login(request))
.expectErrorMatches(error -> error instanceof ApplicationException &&
((ApplicationException) error).getExceptionCode()
.equals(CLIENT_INVALID_USERNAME_OR_PASSWORD))
.verify();
verify(clientUserRepository, never()).findByUsername(anyString());
}
@Test
void testLoginWithNullPassword() {
LoginRequest request = new LoginRequest();
request.setLoginType("password");
request.setIpAddress("192.168.1.1");
Map<String, Object> data = new HashMap<>();
data.put("username", "testuser");
data.put("password", null);
request.setData(data);
StepVerifier.create(usernamePasswordLoginStrategy.login(request))
.expectErrorMatches(error -> error instanceof ApplicationException &&
((ApplicationException) error).getExceptionCode()
.equals(CLIENT_INVALID_USERNAME_OR_PASSWORD))
.verify();
verify(clientUserRepository, never()).findByUsername(anyString());
}
@Test
void testLoginWithEmptyPassword() {
LoginRequest request = new LoginRequest();
request.setLoginType("password");
request.setIpAddress("192.168.1.1");
Map<String, Object> data = new HashMap<>();
data.put("username", "testuser");
data.put("password", "");
request.setData(data);
StepVerifier.create(usernamePasswordLoginStrategy.login(request))
.expectErrorMatches(error -> error instanceof ApplicationException &&
((ApplicationException) error).getExceptionCode()
.equals(CLIENT_INVALID_USERNAME_OR_PASSWORD))
.verify();
verify(clientUserRepository, never()).findByUsername(anyString());
}
@Test
void testLoginWithInvalidData() {
LoginRequest request = new LoginRequest();
request.setLoginType("password");
request.setIpAddress("192.168.1.1");
request.setData("invalid_data");
StepVerifier.create(usernamePasswordLoginStrategy.login(request))
.expectErrorMatches(error -> error instanceof ApplicationException &&
((ApplicationException) error).getExceptionCode()
.equals(CLIENT_INVALID_USERNAME_OR_PASSWORD))
.verify();
verify(clientUserRepository, never()).findByUsername(anyString());
}
@Test
void testLoginWithUserNotFound() {
LoginRequest request = new LoginRequest();
request.setLoginType("password");
request.setIpAddress("192.168.1.1");
Map<String, Object> data = new HashMap<>();
data.put("username", "nonexistent");
data.put("password", "password123");
request.setData(data);
when(clientUserRepository.findByUsername("nonexistent")).thenReturn(Mono.empty());
StepVerifier.create(usernamePasswordLoginStrategy.login(request))
.expectErrorMatches(error -> error instanceof ApplicationException &&
((ApplicationException) error).getExceptionCode()
.equals(CLIENT_INVALID_USERNAME_OR_PASSWORD))
.verify();
verify(clientUserRepository).findByUsername("nonexistent");
verify(passwordEncoder, never()).matches(anyString(), anyString());
}
@Test
void testLoginWithWrongPassword() {
LoginRequest request = new LoginRequest();
request.setLoginType("password");
request.setIpAddress("192.168.1.1");
Map<String, Object> data = new HashMap<>();
data.put("username", "testuser");
data.put("password", "wrong_password");
request.setData(data);
ClientUser user = new ClientUser();
user.setId(1L);
user.setUsername("testuser");
user.setPassword("encoded_password");
when(clientUserRepository.findByUsername("testuser")).thenReturn(Mono.just(user));
when(passwordEncoder.matches("wrong_password", "encoded_password")).thenReturn(false);
StepVerifier.create(usernamePasswordLoginStrategy.login(request))
.expectErrorMatches(error -> error instanceof ApplicationException &&
((ApplicationException) error).getExceptionCode()
.equals(CLIENT_INVALID_USERNAME_OR_PASSWORD))
.verify();
verify(clientUserRepository).findByUsername("testuser");
verify(passwordEncoder).matches("wrong_password", "encoded_password");
verify(clientLoginLogRepository, never()).save(any(ClientLoginLog.class));
}
@Test
void testLoginWithNullIpAddress() {
when(jwtTokenProvider.generateToken(anyLong(), anyString())).thenReturn("test-token");
LoginRequest request = new LoginRequest();
request.setLoginType("password");
request.setIpAddress(null);
Map<String, Object> data = new HashMap<>();
data.put("username", "testuser");
data.put("password", "password123");
request.setData(data);
ClientUser user = new ClientUser();
user.setId(1L);
user.setUsername("testuser");
user.setPassword("encoded_password");
when(clientUserRepository.findByUsername("testuser")).thenReturn(Mono.just(user));
when(passwordEncoder.matches("password123", "encoded_password")).thenReturn(true);
when(clientLoginLogRepository.save(any(ClientLoginLog.class)))
.thenReturn(Mono.just(new ClientLoginLog()));
StepVerifier.create(usernamePasswordLoginStrategy.login(request))
.expectNextMatches(result -> result.getUserId().equals(1L) &&
result.getUsername().equals("testuser") &&
result.getPlatform().equals("password"))
.verifyComplete();
verify(clientUserRepository).findByUsername("testuser");
verify(passwordEncoder).matches("password123", "encoded_password");
verify(clientLoginLogRepository).save(any(ClientLoginLog.class));
}
}
@@ -0,0 +1,328 @@
package io.destiny.client.core.strategy;
import io.destiny.client.client.WechatMiniProgramClient;
import io.destiny.client.core.domain.ClientUser;
import io.destiny.client.core.domain.UserBinding;
import io.destiny.client.core.repository.IClientUserRepository;
import io.destiny.client.core.repository.IUserBindingRepository;
import io.destiny.client.core.service.ClientUserAuthService;
import io.destiny.client.dto.LoginRequest;
import io.destiny.client.dto.WechatAuthResponse;
import io.destiny.client.dto.WechatLoginRequest;
import io.destiny.client.security.JwtTokenProvider;
import io.destiny.common.exception.ApplicationException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static io.destiny.client.core.exception.ClientUserAuthExceptionCode.CLIENT_INVALID_WECHAT_CODE;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class WechatLoginStrategyTest {
@Mock
private ClientUserAuthService clientUserAuthService;
@Mock
private JwtTokenProvider jwtTokenProvider;
@Mock
private WechatMiniProgramClient wechatClient;
@Mock
private IClientUserRepository clientUserRepository;
@Mock
private IUserBindingRepository userBindingRepository;
@InjectMocks
private WechatLoginStrategy wechatLoginStrategy;
@BeforeEach
void setUp() {
}
@Test
void testGetLoginType() {
assertEquals("wechat", wechatLoginStrategy.getLoginType());
}
@Test
void testValidateRequestWithValidData() {
LoginRequest request = new LoginRequest();
WechatLoginRequest wechatRequest = new WechatLoginRequest();
wechatRequest.setCode("valid_code");
request.setData(wechatRequest);
StepVerifier.create(wechatLoginStrategy.validateRequest(request))
.expectNext(request)
.verifyComplete();
}
@Test
void testValidateRequestWithNullCode() {
LoginRequest request = new LoginRequest();
WechatLoginRequest wechatRequest = new WechatLoginRequest();
wechatRequest.setCode(null);
request.setData(wechatRequest);
StepVerifier.create(wechatLoginStrategy.validateRequest(request))
.expectErrorMatches(error -> error instanceof ApplicationException &&
((ApplicationException) error).getExceptionCode()
.equals(CLIENT_INVALID_WECHAT_CODE))
.verify();
}
@Test
void testValidateRequestWithEmptyCode() {
LoginRequest request = new LoginRequest();
WechatLoginRequest wechatRequest = new WechatLoginRequest();
wechatRequest.setCode("");
request.setData(wechatRequest);
StepVerifier.create(wechatLoginStrategy.validateRequest(request))
.expectErrorMatches(error -> error instanceof ApplicationException &&
((ApplicationException) error).getExceptionCode()
.equals(CLIENT_INVALID_WECHAT_CODE))
.verify();
}
@Test
void testValidateRequestWithInvalidDataType() {
LoginRequest request = new LoginRequest();
request.setData("invalid_data");
StepVerifier.create(wechatLoginStrategy.validateRequest(request))
.expectErrorMatches(error -> error instanceof ApplicationException &&
((ApplicationException) error).getExceptionCode()
.equals(CLIENT_INVALID_WECHAT_CODE))
.verify();
}
@Test
void testLoginSuccessWithExistingUserByOpenId() {
when(jwtTokenProvider.generateToken(anyLong(), anyString())).thenReturn("test-token");
LoginRequest request = new LoginRequest();
request.setLoginType("wechat");
request.setIpAddress("192.168.1.1");
WechatLoginRequest wechatRequest = new WechatLoginRequest();
wechatRequest.setCode("valid_code");
request.setData(wechatRequest);
WechatAuthResponse authResponse = new WechatAuthResponse();
authResponse.setOpenId("open_id_123");
authResponse.setUnionId("union_id_456");
authResponse.setSessionKey("session_key");
ClientUser user = new ClientUser();
user.setId(1L);
user.setUsername("testuser");
user.setWechatOpenId("open_id_123");
when(wechatClient.auth("valid_code")).thenReturn(Mono.just(authResponse));
when(clientUserRepository.findByWechatOpenId("open_id_123")).thenReturn(Mono.just(user));
StepVerifier.create(wechatLoginStrategy.login(request))
.expectNextMatches(result -> result.getUserId().equals(1L) &&
result.getUsername().equals("testuser") &&
result.getPlatform().equals("wechat") &&
result.getToken().equals("test-token"))
.verifyComplete();
verify(clientUserRepository, never()).findByWechatUnionId(anyString());
verify(userBindingRepository, never()).findByPlatformAndUnionId(anyString(), anyString());
verify(clientUserRepository, never()).save(any(ClientUser.class));
}
@Test
void testLoginSuccessWithExistingUserByUnionId() {
when(jwtTokenProvider.generateToken(anyLong(), anyString())).thenReturn("test-token");
LoginRequest request = new LoginRequest();
request.setLoginType("wechat");
request.setIpAddress("192.168.1.1");
WechatLoginRequest wechatRequest = new WechatLoginRequest();
wechatRequest.setCode("valid_code");
request.setData(wechatRequest);
WechatAuthResponse authResponse = new WechatAuthResponse();
authResponse.setOpenId("open_id_123");
authResponse.setUnionId("union_id_456");
authResponse.setSessionKey("session_key");
ClientUser user = new ClientUser();
user.setId(1L);
user.setUsername("testuser");
user.setWechatUnionId("union_id_456");
when(wechatClient.auth("valid_code")).thenReturn(Mono.just(authResponse));
when(clientUserRepository.findByWechatOpenId("open_id_123")).thenReturn(Mono.empty());
when(clientUserRepository.findByWechatUnionId("union_id_456")).thenReturn(Mono.just(user));
StepVerifier.create(wechatLoginStrategy.login(request))
.expectNextMatches(result -> result.getUserId().equals(1L) &&
result.getUsername().equals("testuser") &&
result.getPlatform().equals("wechat") &&
result.getToken().equals("test-token"))
.verifyComplete();
verify(userBindingRepository, never()).findByPlatformAndUnionId(anyString(), anyString());
verify(clientUserRepository, never()).save(any(ClientUser.class));
}
@Test
void testLoginSuccessWithExistingUserByBinding() {
when(jwtTokenProvider.generateToken(anyLong(), anyString())).thenReturn("test-token");
LoginRequest request = new LoginRequest();
request.setLoginType("wechat");
request.setIpAddress("192.168.1.1");
WechatLoginRequest wechatRequest = new WechatLoginRequest();
wechatRequest.setCode("valid_code");
request.setData(wechatRequest);
WechatAuthResponse authResponse = new WechatAuthResponse();
authResponse.setOpenId("open_id_123");
authResponse.setUnionId("union_id_456");
authResponse.setSessionKey("session_key");
ClientUser user = new ClientUser();
user.setId(1L);
user.setUsername("testuser");
UserBinding binding = new UserBinding();
binding.setUserId(1L);
binding.setPlatform("wechat");
binding.setPlatformUnionId("union_id_456");
when(wechatClient.auth("valid_code")).thenReturn(Mono.just(authResponse));
when(clientUserRepository.findByWechatOpenId("open_id_123")).thenReturn(Mono.empty());
when(clientUserRepository.findByWechatUnionId("union_id_456")).thenReturn(Mono.empty());
when(userBindingRepository.findByPlatformAndUnionId("wechat", "union_id_456"))
.thenReturn(Mono.just(binding));
when(clientUserRepository.findById(1L)).thenReturn(Mono.just(user));
StepVerifier.create(wechatLoginStrategy.login(request))
.expectNextMatches(result -> result.getUserId().equals(1L) &&
result.getUsername().equals("testuser") &&
result.getPlatform().equals("wechat") &&
result.getToken().equals("test-token"))
.verifyComplete();
verify(clientUserRepository, never()).save(any(ClientUser.class));
}
@Test
void testLoginSuccessWithNewUser() {
when(jwtTokenProvider.generateToken(anyLong(), anyString())).thenReturn("test-token");
LoginRequest request = new LoginRequest();
request.setLoginType("wechat");
request.setIpAddress("192.168.1.1");
WechatLoginRequest wechatRequest = new WechatLoginRequest();
wechatRequest.setCode("valid_code");
request.setData(wechatRequest);
WechatAuthResponse authResponse = new WechatAuthResponse();
authResponse.setOpenId("open_id_123");
authResponse.setUnionId("union_id_456");
authResponse.setSessionKey("session_key");
ClientUser newUser = new ClientUser();
newUser.setId(1L);
newUser.setWechatOpenId("open_id_123");
newUser.setWechatUnionId("union_id_456");
newUser.setPhoneVerified(false);
UserBinding binding = new UserBinding();
binding.setId(1L);
binding.setUserId(1L);
binding.setPlatform("wechat");
binding.setPlatformOpenId("open_id_123");
binding.setPlatformUnionId("union_id_456");
when(wechatClient.auth("valid_code")).thenReturn(Mono.just(authResponse));
when(clientUserRepository.findByWechatOpenId("open_id_123")).thenReturn(Mono.empty());
when(clientUserRepository.findByWechatUnionId("union_id_456")).thenReturn(Mono.empty());
when(userBindingRepository.findByPlatformAndUnionId("wechat", "union_id_456")).thenReturn(Mono.empty());
when(clientUserRepository.save(any(ClientUser.class))).thenReturn(Mono.just(newUser));
when(userBindingRepository.save(any(UserBinding.class))).thenReturn(Mono.just(binding));
StepVerifier.create(wechatLoginStrategy.login(request))
.expectNextMatches(result -> result.getUserId().equals(1L) &&
result.getPlatform().equals("wechat") &&
result.getToken().equals("test-token"))
.verifyComplete();
verify(clientUserRepository).save(any(ClientUser.class));
verify(userBindingRepository).save(any(UserBinding.class));
}
@Test
void testLoginWithNewUserWithoutUnionId() {
when(jwtTokenProvider.generateToken(anyLong(), anyString())).thenReturn("test-token");
LoginRequest request = new LoginRequest();
request.setLoginType("wechat");
request.setIpAddress("192.168.1.1");
WechatLoginRequest wechatRequest = new WechatLoginRequest();
wechatRequest.setCode("valid_code");
request.setData(wechatRequest);
WechatAuthResponse authResponse = new WechatAuthResponse();
authResponse.setOpenId("open_id_123");
authResponse.setSessionKey("session_key");
ClientUser newUser = new ClientUser();
newUser.setId(1L);
newUser.setWechatOpenId("open_id_123");
newUser.setPhoneVerified(false);
UserBinding binding = new UserBinding();
binding.setId(1L);
binding.setUserId(1L);
binding.setPlatform("wechat");
binding.setPlatformOpenId("open_id_123");
when(wechatClient.auth("valid_code")).thenReturn(Mono.just(authResponse));
when(clientUserRepository.findByWechatOpenId("open_id_123")).thenReturn(Mono.empty());
when(clientUserRepository.save(any(ClientUser.class))).thenReturn(Mono.just(newUser));
when(userBindingRepository.save(any(UserBinding.class))).thenReturn(Mono.just(binding));
StepVerifier.create(wechatLoginStrategy.login(request))
.expectNextMatches(result -> result.getUserId().equals(1L) &&
result.getPlatform().equals("wechat") &&
result.getToken().equals("test-token"))
.verifyComplete();
verify(clientUserRepository, never()).findByWechatUnionId(anyString());
verify(userBindingRepository, never()).findByPlatformAndUnionId(anyString(), anyString());
}
@Test
void testLoginWithAuthFailure() {
LoginRequest request = new LoginRequest();
request.setLoginType("wechat");
request.setIpAddress("192.168.1.1");
WechatLoginRequest wechatRequest = new WechatLoginRequest();
wechatRequest.setCode("invalid_code");
request.setData(wechatRequest);
when(wechatClient.auth("invalid_code")).thenReturn(Mono.error(new Exception("Auth failed")));
StepVerifier.create(wechatLoginStrategy.login(request))
.expectError(Exception.class)
.verify();
}
}
@@ -0,0 +1,49 @@
package io.destiny.client.dto;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class AccountBindRequestTest {
@Test
void testGettersAndSetters() {
AccountBindRequest request = new AccountBindRequest();
request.setPlatform("wechat");
request.setCode("test_code_123");
assertEquals("wechat", request.getPlatform());
assertEquals("test_code_123", request.getCode());
}
@Test
void testDefaultValues() {
AccountBindRequest request = new AccountBindRequest();
assertNull(request.getPlatform());
assertNull(request.getCode());
}
@Test
void testSetNullValues() {
AccountBindRequest request = new AccountBindRequest();
request.setPlatform(null);
request.setCode(null);
assertNull(request.getPlatform());
assertNull(request.getCode());
}
@Test
void testSetEmptyValues() {
AccountBindRequest request = new AccountBindRequest();
request.setPlatform("");
request.setCode("");
assertEquals("", request.getPlatform());
assertEquals("", request.getCode());
}
}
@@ -0,0 +1,86 @@
package io.destiny.client.dto;
import io.destiny.client.core.domain.query.ClientUserLogin;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ClientUserLoginRequestTest {
@Test
void testGettersAndSetters() {
ClientUserLoginRequest request = new ClientUserLoginRequest();
request.setUsername("testuser");
request.setPassword("password123");
request.setIpAddress("192.168.1.1");
assertEquals("testuser", request.getUsername());
assertEquals("password123", request.getPassword());
assertEquals("192.168.1.1", request.getIpAddress());
}
@Test
void testDefaultValues() {
ClientUserLoginRequest request = new ClientUserLoginRequest();
assertNull(request.getUsername());
assertNull(request.getPassword());
assertNull(request.getIpAddress());
}
@Test
void testSetNullValues() {
ClientUserLoginRequest request = new ClientUserLoginRequest();
request.setUsername(null);
request.setPassword(null);
request.setIpAddress(null);
assertNull(request.getUsername());
assertNull(request.getPassword());
assertNull(request.getIpAddress());
}
@Test
void testSetEmptyValues() {
ClientUserLoginRequest request = new ClientUserLoginRequest();
request.setUsername("");
request.setPassword("");
request.setIpAddress("");
assertEquals("", request.getUsername());
assertEquals("", request.getPassword());
assertEquals("", request.getIpAddress());
}
@Test
void testToDomain() {
ClientUserLoginRequest request = new ClientUserLoginRequest();
request.setUsername("testuser");
request.setPassword("password123");
request.setIpAddress("192.168.1.1");
ClientUserLogin domain = request.toDomain();
assertNotNull(domain);
assertEquals("testuser", domain.getUsername());
assertEquals("password123", domain.getPassword());
assertNotNull(domain.getIpAddress());
assertEquals("192.168.1.1", domain.getIpAddress().getValue());
}
@Test
void testToDomainWithNullValues() {
ClientUserLoginRequest request = new ClientUserLoginRequest();
ClientUserLogin domain = request.toDomain();
assertNotNull(domain);
assertNull(domain.getUsername());
assertNull(domain.getPassword());
assertNull(domain.getIpAddress());
}
}
@@ -0,0 +1,123 @@
package io.destiny.client.dto;
import io.destiny.client.core.domain.ClientUser;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ClientUserRegisterRequestTest {
@Test
void testGettersAndSetters() {
ClientUserRegisterRequest request = new ClientUserRegisterRequest();
request.setUsername("testuser");
request.setPassword("password123");
request.setEmail("test@example.com");
request.setPhone("13800138000");
request.setGender("male");
assertEquals("testuser", request.getUsername());
assertEquals("password123", request.getPassword());
assertEquals("test@example.com", request.getEmail());
assertEquals("13800138000", request.getPhone());
assertEquals("male", request.getGender());
}
@Test
void testDefaultValues() {
ClientUserRegisterRequest request = new ClientUserRegisterRequest();
assertNull(request.getUsername());
assertNull(request.getPassword());
assertNull(request.getEmail());
assertNull(request.getPhone());
assertNull(request.getGender());
}
@Test
void testSetNullValues() {
ClientUserRegisterRequest request = new ClientUserRegisterRequest();
request.setUsername(null);
request.setPassword(null);
request.setEmail(null);
request.setPhone(null);
request.setGender(null);
assertNull(request.getUsername());
assertNull(request.getPassword());
assertNull(request.getEmail());
assertNull(request.getPhone());
assertNull(request.getGender());
}
@Test
void testSetEmptyValues() {
ClientUserRegisterRequest request = new ClientUserRegisterRequest();
request.setUsername("");
request.setPassword("");
request.setEmail("");
request.setPhone("");
request.setGender("");
assertEquals("", request.getUsername());
assertEquals("", request.getPassword());
assertEquals("", request.getEmail());
assertEquals("", request.getPhone());
assertEquals("", request.getGender());
}
@Test
void testToDomain() {
ClientUserRegisterRequest request = new ClientUserRegisterRequest();
request.setUsername("testuser");
request.setPassword("password123");
request.setEmail("test@example.com");
request.setPhone("13800138000");
request.setGender("male");
ClientUser domain = request.toDomain();
assertNotNull(domain);
assertEquals("testuser", domain.getUsername());
assertEquals("password123", domain.getPassword());
assertNotNull(domain.getEmail());
assertEquals("test@example.com", domain.getEmail().getValue());
assertNotNull(domain.getPhone());
assertEquals("13800138000", domain.getPhone().getValue());
assertEquals("male", domain.getGender());
}
@Test
void testToDomainWithNullValues() {
ClientUserRegisterRequest request = new ClientUserRegisterRequest();
ClientUser domain = request.toDomain();
assertNotNull(domain);
assertNull(domain.getUsername());
assertNull(domain.getPassword());
assertNull(domain.getEmail());
assertNull(domain.getPhone());
assertNull(domain.getGender());
}
@Test
void testToDomainWithPartialValues() {
ClientUserRegisterRequest request = new ClientUserRegisterRequest();
request.setUsername("testuser");
request.setPassword("password123");
ClientUser domain = request.toDomain();
assertNotNull(domain);
assertEquals("testuser", domain.getUsername());
assertEquals("password123", domain.getPassword());
assertNull(domain.getEmail());
assertNull(domain.getPhone());
assertNull(domain.getGender());
}
}
@@ -0,0 +1,56 @@
package io.destiny.client.dto;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class DouyinLoginRequestTest {
@Test
void testGettersAndSetters() {
DouyinLoginRequest request = new DouyinLoginRequest();
request.setCode("test_code_123");
request.setPlatform("douyin");
request.setIpAddress("192.168.1.1");
assertEquals("test_code_123", request.getCode());
assertEquals("douyin", request.getPlatform());
assertEquals("192.168.1.1", request.getIpAddress());
}
@Test
void testDefaultValues() {
DouyinLoginRequest request = new DouyinLoginRequest();
assertNull(request.getCode());
assertNull(request.getPlatform());
assertNull(request.getIpAddress());
}
@Test
void testSetNullValues() {
DouyinLoginRequest request = new DouyinLoginRequest();
request.setCode(null);
request.setPlatform(null);
request.setIpAddress(null);
assertNull(request.getCode());
assertNull(request.getPlatform());
assertNull(request.getIpAddress());
}
@Test
void testSetEmptyValues() {
DouyinLoginRequest request = new DouyinLoginRequest();
request.setCode("");
request.setPlatform("");
request.setIpAddress("");
assertEquals("", request.getCode());
assertEquals("", request.getPlatform());
assertEquals("", request.getIpAddress());
}
}
@@ -0,0 +1,72 @@
package io.destiny.client.dto;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class LoginRequestTest {
@Test
void testGettersAndSetters() {
LoginRequest request = new LoginRequest();
request.setLoginType("sms");
request.setIpAddress("192.168.1.1");
request.setData(new SmsLoginRequest());
assertEquals("sms", request.getLoginType());
assertEquals("192.168.1.1", request.getIpAddress());
assertNotNull(request.getData());
assertTrue(request.getData() instanceof SmsLoginRequest);
}
@Test
void testDefaultValues() {
LoginRequest request = new LoginRequest();
assertNull(request.getLoginType());
assertNull(request.getIpAddress());
assertNull(request.getData());
}
@Test
void testSetNullValues() {
LoginRequest request = new LoginRequest();
request.setLoginType(null);
request.setIpAddress(null);
request.setData(null);
assertNull(request.getLoginType());
assertNull(request.getIpAddress());
assertNull(request.getData());
}
@Test
void testSetEmptyValues() {
LoginRequest request = new LoginRequest();
request.setLoginType("");
request.setIpAddress("");
assertEquals("", request.getLoginType());
assertEquals("", request.getIpAddress());
}
@Test
void testSetDifferentDataTypes() {
LoginRequest request = new LoginRequest();
SmsLoginRequest smsRequest = new SmsLoginRequest();
request.setData(smsRequest);
assertEquals(smsRequest, request.getData());
PasswordLoginRequest passwordRequest = new PasswordLoginRequest();
request.setData(passwordRequest);
assertEquals(passwordRequest, request.getData());
WechatLoginRequest wechatRequest = new WechatLoginRequest();
request.setData(wechatRequest);
assertEquals(wechatRequest, request.getData());
}
}
@@ -0,0 +1,56 @@
package io.destiny.client.dto;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class PasswordLoginRequestTest {
@Test
void testGettersAndSetters() {
PasswordLoginRequest request = new PasswordLoginRequest();
request.setUsername("testuser");
request.setPassword("password123");
request.setIpAddress("192.168.1.1");
assertEquals("testuser", request.getUsername());
assertEquals("password123", request.getPassword());
assertEquals("192.168.1.1", request.getIpAddress());
}
@Test
void testDefaultValues() {
PasswordLoginRequest request = new PasswordLoginRequest();
assertNull(request.getUsername());
assertNull(request.getPassword());
assertNull(request.getIpAddress());
}
@Test
void testSetNullValues() {
PasswordLoginRequest request = new PasswordLoginRequest();
request.setUsername(null);
request.setPassword(null);
request.setIpAddress(null);
assertNull(request.getUsername());
assertNull(request.getPassword());
assertNull(request.getIpAddress());
}
@Test
void testSetEmptyValues() {
PasswordLoginRequest request = new PasswordLoginRequest();
request.setUsername("");
request.setPassword("");
request.setIpAddress("");
assertEquals("", request.getUsername());
assertEquals("", request.getPassword());
assertEquals("", request.getIpAddress());
}
}
@@ -0,0 +1,49 @@
package io.destiny.client.dto;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class SmsCodeSendRequestTest {
@Test
void testGettersAndSetters() {
SmsCodeSendRequest request = new SmsCodeSendRequest();
request.setPhone("13800138000");
request.setType("login");
assertEquals("13800138000", request.getPhone());
assertEquals("login", request.getType());
}
@Test
void testDefaultValues() {
SmsCodeSendRequest request = new SmsCodeSendRequest();
assertNull(request.getPhone());
assertNull(request.getType());
}
@Test
void testSetNullValues() {
SmsCodeSendRequest request = new SmsCodeSendRequest();
request.setPhone(null);
request.setType(null);
assertNull(request.getPhone());
assertNull(request.getType());
}
@Test
void testSetEmptyValues() {
SmsCodeSendRequest request = new SmsCodeSendRequest();
request.setPhone("");
request.setType("");
assertEquals("", request.getPhone());
assertEquals("", request.getType());
}
}
@@ -0,0 +1,56 @@
package io.destiny.client.dto;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class SmsLoginRequestTest {
@Test
void testGettersAndSetters() {
SmsLoginRequest request = new SmsLoginRequest();
request.setPhone("13800138000");
request.setCode("123456");
request.setIpAddress("192.168.1.1");
assertEquals("13800138000", request.getPhone());
assertEquals("123456", request.getCode());
assertEquals("192.168.1.1", request.getIpAddress());
}
@Test
void testDefaultValues() {
SmsLoginRequest request = new SmsLoginRequest();
assertNull(request.getPhone());
assertNull(request.getCode());
assertNull(request.getIpAddress());
}
@Test
void testSetNullValues() {
SmsLoginRequest request = new SmsLoginRequest();
request.setPhone(null);
request.setCode(null);
request.setIpAddress(null);
assertNull(request.getPhone());
assertNull(request.getCode());
assertNull(request.getIpAddress());
}
@Test
void testSetEmptyValues() {
SmsLoginRequest request = new SmsLoginRequest();
request.setPhone("");
request.setCode("");
request.setIpAddress("");
assertEquals("", request.getPhone());
assertEquals("", request.getCode());
assertEquals("", request.getIpAddress());
}
}
@@ -0,0 +1,56 @@
package io.destiny.client.dto;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class WechatLoginRequestTest {
@Test
void testGettersAndSetters() {
WechatLoginRequest request = new WechatLoginRequest();
request.setCode("test_code_123");
request.setPlatform("wechat");
request.setIpAddress("192.168.1.1");
assertEquals("test_code_123", request.getCode());
assertEquals("wechat", request.getPlatform());
assertEquals("192.168.1.1", request.getIpAddress());
}
@Test
void testDefaultValues() {
WechatLoginRequest request = new WechatLoginRequest();
assertNull(request.getCode());
assertNull(request.getPlatform());
assertNull(request.getIpAddress());
}
@Test
void testSetNullValues() {
WechatLoginRequest request = new WechatLoginRequest();
request.setCode(null);
request.setPlatform(null);
request.setIpAddress(null);
assertNull(request.getCode());
assertNull(request.getPlatform());
assertNull(request.getIpAddress());
}
@Test
void testSetEmptyValues() {
WechatLoginRequest request = new WechatLoginRequest();
request.setCode("");
request.setPlatform("");
request.setIpAddress("");
assertEquals("", request.getCode());
assertEquals("", request.getPlatform());
assertEquals("", request.getIpAddress());
}
}
@@ -0,0 +1,125 @@
package io.destiny.client.security;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class JwtTokenProviderTest {
@InjectMocks
private JwtTokenProvider jwtTokenProvider;
private static final String SECRET_KEY = "your-secret-key-must-be-at-least-256-bits-long-for-hs256-algorithm";
private static final Long EXPIRATION = 86400000L;
private static final Long USER_ID = 123L;
private static final String USERNAME = "testuser";
@BeforeEach
void setUp() {
ReflectionTestUtils.setField(jwtTokenProvider, "secret", SECRET_KEY);
ReflectionTestUtils.setField(jwtTokenProvider, "expiration", EXPIRATION);
}
@Test
void testGenerateToken_Success() {
String token = jwtTokenProvider.generateToken(USER_ID, USERNAME);
assertNotNull(token);
assertFalse(token.isEmpty());
}
@Test
void testGetUserIdFromToken_Success() {
String token = jwtTokenProvider.generateToken(USER_ID, USERNAME);
Long userId = jwtTokenProvider.getUserIdFromToken(token);
assertEquals(USER_ID, userId);
}
@Test
void testGetUserIdFromToken_InvalidToken() {
Long userId = jwtTokenProvider.getUserIdFromToken("invalid.token.here");
assertNull(userId);
}
@Test
void testGetUserIdFromToken_NullToken() {
Long userId = jwtTokenProvider.getUserIdFromToken(null);
assertNull(userId);
}
@Test
void testGetUsernameFromToken_Success() {
String token = jwtTokenProvider.generateToken(USER_ID, USERNAME);
String username = jwtTokenProvider.getUsernameFromToken(token);
assertEquals(USERNAME, username);
}
@Test
void testGetUsernameFromToken_InvalidToken() {
String username = jwtTokenProvider.getUsernameFromToken("invalid.token.here");
assertNull(username);
}
@Test
void testGetUsernameFromToken_NullToken() {
String username = jwtTokenProvider.getUsernameFromToken(null);
assertNull(username);
}
@Test
void testValidateToken_Success() {
String token = jwtTokenProvider.generateToken(USER_ID, USERNAME);
Boolean isValid = jwtTokenProvider.validateToken(token);
assertTrue(isValid);
}
@Test
void testValidateToken_InvalidToken() {
Boolean isValid = jwtTokenProvider.validateToken("invalid.token.here");
assertFalse(isValid);
}
@Test
void testValidateToken_NullToken() {
Boolean isValid = jwtTokenProvider.validateToken(null);
assertFalse(isValid);
}
@Test
void testIsTokenExpired_False() {
String token = jwtTokenProvider.generateToken(USER_ID, USERNAME);
Boolean isExpired = jwtTokenProvider.isTokenExpired(token);
assertFalse(isExpired);
}
@Test
void testIsTokenExpired_InvalidToken() {
Boolean isExpired = jwtTokenProvider.isTokenExpired("invalid.token.here");
assertTrue(isExpired);
}
@Test
void testIsTokenExpired_NullToken() {
Boolean isExpired = jwtTokenProvider.isTokenExpired(null);
assertTrue(isExpired);
}
@Test
void testGenerateToken_DifferentUsers() {
String token1 = jwtTokenProvider.generateToken(1L, "user1");
String token2 = jwtTokenProvider.generateToken(2L, "user2");
assertNotEquals(token1, token2);
}
@Test
void testGetUserIdFromToken_CorrectUser() {
String token = jwtTokenProvider.generateToken(USER_ID, USERNAME);
Long userId = jwtTokenProvider.getUserIdFromToken(token);
assertEquals(USER_ID, userId);
}
}