* 支持多角色数据权限合并(并集策略): * - 如果任一角色是 ALL,则跳过数据权限过滤 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")); + } + +} From 155fc69a78e19a1dbca9ba1445c8cd4598bf43ed Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Sun, 2 Aug 2026 14:18:44 +0800 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=20project.ve?= =?UTF-8?q?rsion=20=E9=85=8D=E7=BD=AE=E9=81=BF=E5=85=8D=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 application.yml 的 project.version 段,banner 与 OpenApi 不再读取该属性 - 移除 banner.txt 的 YouLai Boot Version 行 - OpenApiConfig 不再显示接口文档版本号,删除无用的 Environment 依赖 - 为 config.import: classpath:codegen.yml 补充代码生成模块配置说明注释 --- src/main/java/com/youlai/boot/config/OpenApiConfig.java | 7 +------ src/main/resources/application.yml | 4 +--- src/main/resources/banner.txt | 1 - 3 files changed, 2 insertions(+), 10 deletions(-) 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/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/ 版权所属: 有来开源组织