diff --git a/pom.xml b/pom.xml index 2d92fe60..7d940ad1 100644 --- a/pom.xml +++ b/pom.xml @@ -106,6 +106,13 @@ test + + + org.springframework.boot + spring-boot-starter-webmvc-test + test + + org.springframework.boot spring-boot-starter-security diff --git a/src/main/java/com/youlai/boot/config/MybatisConfig.java b/src/main/java/com/youlai/boot/config/MybatisConfig.java index c337dbb2..edf88837 100644 --- a/src/main/java/com/youlai/boot/config/MybatisConfig.java +++ b/src/main/java/com/youlai/boot/config/MybatisConfig.java @@ -5,8 +5,8 @@ import com.baomidou.mybatisplus.core.config.GlobalConfig; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; -import com.youlai.boot.framework.mybatis.handler.AutoFillMetaObjectHandler; -import com.youlai.boot.framework.mybatis.interceptor.MyDataPermissionHandler; +import com.youlai.boot.framework.mybatis.MetaFieldFillHandler; +import com.youlai.boot.framework.mybatis.MyDataPermissionHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -43,7 +43,7 @@ public class MybatisConfig { @Bean public GlobalConfig globalConfig() { GlobalConfig globalConfig = new GlobalConfig(); - globalConfig.setMetaObjectHandler(new AutoFillMetaObjectHandler()); + globalConfig.setMetaObjectHandler(new MetaFieldFillHandler()); return globalConfig; } diff --git a/src/main/java/com/youlai/boot/config/OpenApiConfig.java b/src/main/java/com/youlai/boot/config/OpenApiConfig.java index 5e6c2d54..3fdcb10a 100644 --- a/src/main/java/com/youlai/boot/config/OpenApiConfig.java +++ b/src/main/java/com/youlai/boot/config/OpenApiConfig.java @@ -14,7 +14,7 @@ import lombok.extern.slf4j.Slf4j; import org.springdoc.core.customizers.GlobalOpenApiCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.Environment; + import org.springframework.http.HttpHeaders; import org.springframework.util.AntPathMatcher; @@ -32,8 +32,6 @@ import java.util.stream.Stream; @Slf4j public class OpenApiConfig { - private final Environment environment; - private final SecurityProperties securityProperties; /** @@ -42,13 +40,10 @@ public class OpenApiConfig { @Bean public OpenAPI openApi() { - String appVersion = environment.getProperty("project.version", "1.0.0"); - return new OpenAPI() .info(new Info() .title("管理系统 API 文档") .description("本文档涵盖管理系统的所有API接口,包括登录认证、用户管理、角色管理、部门管理等功能模块,提供详细的接口说明和使用指南。") - .version(appVersion) .license(new License() .name("Apache License 2.0") .url("http://www.apache.org/licenses/LICENSE-2.0") diff --git a/src/main/java/com/youlai/boot/framework/mybatis/handler/AutoFillMetaObjectHandler.java b/src/main/java/com/youlai/boot/framework/mybatis/MetaFieldFillHandler.java similarity index 91% rename from src/main/java/com/youlai/boot/framework/mybatis/handler/AutoFillMetaObjectHandler.java rename to src/main/java/com/youlai/boot/framework/mybatis/MetaFieldFillHandler.java index 80d260e6..cc8fe5c5 100644 --- a/src/main/java/com/youlai/boot/framework/mybatis/handler/AutoFillMetaObjectHandler.java +++ b/src/main/java/com/youlai/boot/framework/mybatis/MetaFieldFillHandler.java @@ -1,4 +1,4 @@ -package com.youlai.boot.framework.mybatis.handler; +package com.youlai.boot.framework.mybatis; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import com.youlai.boot.framework.security.util.SecurityUtils; @@ -17,7 +17,7 @@ import java.time.LocalDateTime; * @since 3.0.0 */ @Component -public class AutoFillMetaObjectHandler implements MetaObjectHandler { +public class MetaFieldFillHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { diff --git a/src/main/java/com/youlai/boot/framework/mybatis/interceptor/MyDataPermissionHandler.java b/src/main/java/com/youlai/boot/framework/mybatis/MyDataPermissionHandler.java similarity index 99% rename from src/main/java/com/youlai/boot/framework/mybatis/interceptor/MyDataPermissionHandler.java rename to src/main/java/com/youlai/boot/framework/mybatis/MyDataPermissionHandler.java index bc2a48b9..38ac05db 100644 --- a/src/main/java/com/youlai/boot/framework/mybatis/interceptor/MyDataPermissionHandler.java +++ b/src/main/java/com/youlai/boot/framework/mybatis/MyDataPermissionHandler.java @@ -1,4 +1,4 @@ -package com.youlai.boot.framework.mybatis.interceptor; +package com.youlai.boot.framework.mybatis; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.StrUtil; @@ -23,7 +23,7 @@ import java.lang.reflect.Method; import java.util.List; /** - * 数据权限控制器 + * 数据权限处理器 *

* 支持多角色数据权限合并(并集策略): * - 如果任一角色是 ALL,则跳过数据权限过滤 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d0e236f3..675fee96 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,12 +4,10 @@ spring: profiles: active: dev config: + # 引入代码生成模块的配置(数据源、表前缀等),由 codegen.yml 提供 import: classpath:codegen.yml servlet: multipart: # 单文件大小上限,取自 file-storage.upload.max-file-size(单一来源) max-file-size: ${file-storage.upload.max-file-size:50MB} max-request-size: ${file-storage.upload.max-file-size:50MB} -# 在 banner.txt 中显示项目版本,使用 @project.version@ 从 pom.xml 获取 -project: - version: @project.version@ diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt index c3c28124..f4ce6a15 100644 --- a/src/main/resources/banner.txt +++ b/src/main/resources/banner.txt @@ -7,7 +7,6 @@ ${AnsiColor.BRIGHT_BLUE} |_|\___/ \__,_| |______\__,_|_| ${AnsiColor.BRIGHT_GREEN} -YouLai Boot Version: ${project.version} Spring Boot Version: ${spring-boot.version}${spring-boot.formatted-version} 有来官网: https://www.youlai.tech/ 版权所属: 有来开源组织 diff --git a/src/test/java/com/youlai/boot/AuthControllerTest.java b/src/test/java/com/youlai/boot/AuthControllerTest.java new file mode 100644 index 00000000..7892c0ae --- /dev/null +++ b/src/test/java/com/youlai/boot/AuthControllerTest.java @@ -0,0 +1,147 @@ +package com.youlai.boot; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * 认证 + 当前用户接口单元测试 + * + * @author Ray.Hao + * @since 4.6.0 + */ +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("dev") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class AuthControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + private static String accessToken; + + @Test + @Order(1) + @DisplayName("登录成功") + void loginSuccess() throws Exception { + String body = """ + { + "username": "admin", + "password": "123456" + } + """; + + MvcResult result = mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("00000")) + .andExpect(jsonPath("$.data.accessToken").isString()) + .andExpect(jsonPath("$.data.refreshToken").isString()) + .andExpect(jsonPath("$.data.tokenType").value("Bearer")) + .andExpect(jsonPath("$.data.expiresIn").isNumber()) + .andReturn(); + + JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString()); + accessToken = root.path("data").path("accessToken").asText(); + + assertNotNull(accessToken, "登录后应返回 accessToken"); + assertFalse(accessToken.isBlank(), "accessToken 不能为空"); + } + + @Test + @Order(2) + @DisplayName("密码错误登录失败") + void loginWithWrongPassword() throws Exception { + String body = """ + { + "username": "admin", + "password": "wrong_password" + } + """; + + mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("A0210")) + .andExpect(jsonPath("$.msg").value("密码错误")); + } + + @Test + @Order(3) + @DisplayName("空用户名登录失败") + void loginWithEmptyUsername() throws Exception { + String body = """ + { + "username": "", + "password": "123456" + } + """; + + mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("B0001")); + } + + @Test + @Order(4) + @DisplayName("登录后获取当前用户信息") + void getCurrentUserWithToken() throws Exception { + assertNotNull(accessToken, "accessToken 应由登录测试先行填充"); + + mockMvc.perform(get("/api/v1/users/me") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("00000")) + .andExpect(jsonPath("$.data.userId").isNumber()) + .andExpect(jsonPath("$.data.username").isString()) + .andExpect(jsonPath("$.data.nickname").isString()) + .andExpect(jsonPath("$.data.roles").isArray()) + .andExpect(jsonPath("$.data.perms").isArray()); + } + + @Test + @Order(5) + @DisplayName("无 Token 请求 /me 返回令牌无效") + void getCurrentUserWithoutToken() throws Exception { + mockMvc.perform(get("/api/v1/users/me")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("A0230")) + .andExpect(jsonPath("$.msg").value("令牌无效或已过期")); + } + + @Test + @Order(6) + @DisplayName("伪造 Token 请求 /me 返回令牌无效") + void getCurrentUserWithInvalidToken() throws Exception { + mockMvc.perform(get("/api/v1/users/me") + .header("Authorization", "Bearer invalid_token_xxx")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("A0230")); + } + +}