Skip to content

Commit 1ed1c5c

Browse files
committed
fix: resolve 36 test failures and add detailed explanatory comments
Root causes and fixes: 1. SaTokenPermissionImpl (NEW) - Without StpInterface, Sa-Token returns empty permissions for all users, causing every @SaCheckPermission check to unconditionally fail with 403. - loginId must be parsed via toString() first: Sa-Token serializes it as String internally even when StpUtil.login(Long) was called. 2. GlobalExceptionHandler - NotPermissionException.getCode() returns the integer scene code (-1), not the permission string. Fixed to getPermission() which returns the actual missing permission name (e.g. "user:center:read"). 3. AbstractWebIntegrationTest + BackendApplicationTests - SPRING_DATASOURCE_URL env var (pointing to Neon PostgreSQL) has higher Spring priority than application-test.properties, causing H2 context load failure. Fixed via @SpringBootTest(properties) which overrides all env vars. - JustAuth UrlValidator rejects localhost redirect URIs; overridden with a syntactically valid placeholder URL. 4. AuthControllerIntegrationTests - Sa-Token NOT_TOKEN scenario now maps to "未提供 Token", not the old generic "未登录或登录状态已失效" message. 5. UserCenterControllerIntegrationTests - All URLs updated: /api/user-center/* paths were removed; current routes are /auth/me, /users, /users/{id}, /users/{id}/authorization. - Permission error message updated to match GlobalExceptionHandler output: "拒绝访问: 缺少权限 [<permission>]". 6. OpenAiStreamControllerIntegrationTests - DTO field renamed: message (String) -> messages (List), aligning with Vercel AI SDK payload format. - Async test uses asyncDispatch two-step pattern (required for StreamingResponseBody); polling getContentAsString() never sees data. - Stub returns OpenAI SSE format choices[0].delta.content so relayEvents() can extract the text and emit Vercel Stream prefix "0:". All 78 tests now pass. https://claude.ai/code/session_016Z9qEQdrSXSTAhCp1YMgnk
1 parent 749c21c commit 1ed1c5c

7 files changed

Lines changed: 236 additions & 28 deletions

File tree

src/main/java/com/involutionhell/backend/common/config/SaTokenPermissionImpl.java

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,25 @@
77

88
/**
99
* Sa-Token 权限与角色加载实现。
10-
* 根据登录用户 ID 从数据库加载其权限集合与角色集合,
11-
* 供 {@code @SaCheckPermission} / {@code @SaCheckRole} 等注解使用。
10+
*
11+
* <h3>为什么需要这个类?</h3>
12+
* <p>Sa-Token 的 {@code @SaCheckPermission} / {@code @SaCheckRole} 注解在执行权限校验时,
13+
* 会调用 {@link StpInterface#getPermissionList} / {@link StpInterface#getRoleList}
14+
* 来获取当前登录用户的权限集合与角色集合。</p>
15+
*
16+
* <p>在此类被添加之前,项目中缺少 {@code StpInterface} 的实现 Bean,
17+
* Sa-Token 回退使用默认的空列表实现,导致所有 {@code @SaCheckPermission} 检查
18+
* 无论用户实际持有什么权限,一律抛出 {@code NotPermissionException}(HTTP 403)。
19+
* 这是一个生产代码 Bug:权限体系在数据库层面已设计完备,但因缺少加载桥梁而完全失效。</p>
20+
*
21+
* <h3>实现逻辑</h3>
22+
* <p>以登录时写入 Sa-Token Session 的用户 ID 为键,从 {@code user_accounts} 表加载
23+
* {@code permissions} 与 {@code roles} 列(逗号分隔字符串已由 Repository 层解析为 Set)。</p>
24+
*
25+
* <h3>loginId 类型说明</h3>
26+
* <p>Sa-Token 在内部将登录 ID 序列化为 {@code String} 存储,因此
27+
* {@code loginId} 参数的运行时类型是 {@code String},而非调用 {@code StpUtil.login(Long)}
28+
* 时传入的 {@code Long}。故此处需要通过 {@code Long.valueOf(loginId.toString())} 转换。</p>
1229
*/
1330
@Component
1431
public class SaTokenPermissionImpl implements StpInterface {
@@ -19,13 +36,27 @@ public SaTokenPermissionImpl(UserAccountRepository userAccountRepository) {
1936
this.userAccountRepository = userAccountRepository;
2037
}
2138

39+
/**
40+
* 返回指定用户拥有的权限码列表。
41+
*
42+
* <p>Sa-Token 每次执行 {@code @SaCheckPermission("user:xxx")} 时都会调用此方法,
43+
* 将返回值与注解中声明的权限码对比,若不包含则抛出 {@code NotPermissionException}。</p>
44+
*
45+
* @param loginId 登录 ID,运行时实际类型为 String(Sa-Token 内部序列化结果)
46+
* @param loginType 登录类型,单端场景下为 "login",此处忽略
47+
*/
2248
@Override
2349
public List<String> getPermissionList(Object loginId, String loginType) {
50+
// loginId 由 Sa-Token 以 String 形式回传,需先 toString() 再解析为 Long
2451
return userAccountRepository.findById(Long.valueOf(loginId.toString()))
2552
.map(account -> List.copyOf(account.permissions()))
2653
.orElse(List.of());
2754
}
2855

56+
/**
57+
* 返回指定用户拥有的角色标识列表,供 {@code @SaCheckRole} 使用。
58+
* 逻辑与 {@link #getPermissionList} 完全对称,仅字段来源不同。
59+
*/
2960
@Override
3061
public List<String> getRoleList(Object loginId, String loginType) {
3162
return userAccountRepository.findById(Long.valueOf(loginId.toString()))

src/main/java/com/involutionhell/backend/common/error/GlobalExceptionHandler.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,15 @@ public ResponseEntity<ApiResponse<Void>> handleNotLoginException(NotLoginExcepti
4141
}
4242

4343
/**
44-
* Sa-Token: 拦截权限不足异常
44+
* Sa-Token: 拦截权限不足异常。
45+
*
46+
* <p><b>为什么用 {@code e.getPermission()} 而不是 {@code e.getCode()}?</b><br>
47+
* {@code NotPermissionException} 继承自 {@code SaTokenException},父类的 {@code getCode()}
48+
* 返回的是异常场景码(整数,如 -1),表示"哪种类型的 Sa-Token 异常",而非权限字符串本身。
49+
* 权限字符串(如 {@code "user:center:read"})存储在 {@code NotPermissionException}
50+
* 自身的 {@code permission} 字段中,须通过 {@code getPermission()} 获取。
51+
* 若误用 {@code getCode()},错误消息将显示为 "拒绝访问: 缺少权限 [-1]",
52+
* 对调用方毫无诊断价值。</p>
4553
*/
4654
@ExceptionHandler(NotPermissionException.class)
4755
public ResponseEntity<ApiResponse<Void>> handleNotPermissionException(NotPermissionException e) {

src/test/java/com/involutionhell/backend/BackendApplicationTests.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,33 @@
44
import org.springframework.boot.test.context.SpringBootTest;
55
import org.springframework.test.context.ActiveProfiles;
66

7-
// 显式覆盖数据源配置,防止 SPRING_DATASOURCE_URL 环境变量(指向生产 PostgreSQL)
8-
// 优先于 application-test.properties,导致上下文加载失败。
7+
/**
8+
* Spring Boot 上下文加载冒烟测试。
9+
*
10+
* <h3>为什么需要在这里覆盖数据源属性?</h3>
11+
* <p>原始代码使用裸 {@code @SpringBootTest},未覆盖任何属性。
12+
* 当测试服务器设置了 {@code SPRING_DATASOURCE_URL} 环境变量(指向生产 Neon PostgreSQL)时,
13+
* 环境变量的优先级高于 {@code application-test.properties},
14+
* Spring 会尝试用 PostgreSQL 驱动连接该 URL,而 H2 驱动拒绝 {@code jdbc:postgresql://} 格式,
15+
* 导致上下文启动失败,报错:
16+
* {@code Driver org.h2.Driver claims to not accept jdbcUrl, jdbc:postgresql://...}。</p>
17+
*
18+
* <p>{@code @SpringBootTest(properties)} 的优先级高于一切外部环境变量,
19+
* 可确保本测试始终在 H2 内存库上运行,与生产数据库完全隔离。</p>
20+
*
21+
* <p>同理,也覆盖了 JustAuth 的 redirect-uri:JustAuth 使用 Apache Commons
22+
* {@code UrlValidator} 在 {@link me.zhyd.oauth.request.AuthGithubRequest} 初始化时
23+
* 校验 redirect-uri,默认拒绝 localhost,故使用格式合法的占位 URL。</p>
24+
*/
925
@SpringBootTest(properties = {
26+
// 覆盖 SPRING_DATASOURCE_URL 环境变量,强制使用 H2,详见类 Javadoc
1027
"spring.datasource.url=jdbc:h2:mem:backend;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE",
1128
"spring.datasource.username=sa",
1229
"spring.datasource.password=",
1330
"spring.datasource.driver-class-name=org.h2.Driver",
1431
"spring.sql.init.mode=always",
1532
"spring.sql.init.schema-locations=classpath:test-schema.sql",
33+
// JustAuth UrlValidator 拒绝 localhost,使用合法占位 URL,不发起实际网络请求
1634
"justauth.type.github.redirect-uri=https://example.com/api/auth/callback/github",
1735
"justauth.type.github.client-id=test-client-id",
1836
"justauth.type.github.client-secret=test-client-secret"
@@ -21,7 +39,7 @@
2139
class BackendApplicationTests {
2240

2341
/**
24-
* 验证 Spring Boot 测试上下文可以正常启动。
42+
* 验证 Spring Boot 测试上下文可以正常启动(所有 Bean 可注入、数据源可连接)
2543
*/
2644
@Test
2745
void contextLoads() {

src/test/java/com/involutionhell/backend/openai/controller/OpenAiStreamControllerIntegrationTests.java

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,47 @@
2121
import org.springframework.http.MediaType;
2222
import org.springframework.test.web.servlet.MvcResult;
2323

24+
/**
25+
* OpenAiStreamController 集成测试。
26+
*
27+
* <h3>三处改动说明</h3>
28+
*
29+
* <h4>1. 请求体字段:message → messages</h4>
30+
* <p>{@link OpenAiStreamRequest} DTO 已从单条字符串字段 {@code message} 重构为
31+
* 多轮对话列表字段 {@code messages},以对齐 Vercel AI SDK 的 payload 格式。
32+
* 旧版测试发送 {@code {"message":"..."}},服务端将 {@code messages} 视为 null/空,
33+
* 触发 {@code @NotEmpty} 校验失败(400),而非进入 SSE 处理流程。</p>
34+
*
35+
* <h4>2. 匿名请求错误消息:"未登录..." → "未提供 Token"</h4>
36+
* <p>与 AuthControllerIntegrationTests 同理,未携带 token 属于 Sa-Token {@code NOT_TOKEN}
37+
* 场景,GlobalExceptionHandler 的当前输出是 "未提供 Token",
38+
* 旧版通用消息已过时。</p>
39+
*
40+
* <h4>3. StreamingResponseBody 的 MockMvc 测试方式:asyncDispatch 模式</h4>
41+
* <p>控制器返回 {@link org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody},
42+
* Spring 将实际写入操作分派到异步线程。MockMvc 对此类异步响应的正确测试步骤是:
43+
* <ol>
44+
* <li>调用 {@code mockMvc.perform(...).andExpect(request().asyncStarted()).andReturn()}
45+
* 触发异步处理,获取 {@code MvcResult};</li>
46+
* <li>再调用 {@code mockMvc.perform(asyncDispatch(mvcResult))} 完成派发,
47+
* 此时响应体才真正写入,可对 status / content 做断言。</li>
48+
* </ol>
49+
* 旧版使用轮询 {@code getContentAsString()} 的方式无法获取到 {@code StreamingResponseBody}
50+
* 写入的内容,测试超时报错 "SSE 响应内容未按预期写入"。</p>
51+
*/
2452
@Import(OpenAiStreamControllerIntegrationTests.OpenAiTestConfiguration.class)
2553
class OpenAiStreamControllerIntegrationTests extends AbstractWebIntegrationTest {
2654

55+
/**
56+
* 验证已登录用户可以发起 SSE 流式请求,并收到 Vercel Stream 格式的响应。
57+
*
58+
* <p>采用 asyncDispatch 两步模式:先启动异步,再派发获取完整响应体。</p>
59+
*/
2760
@Test
2861
void streamReturnsSseEventsForAuthenticatedUser() throws Exception {
2962
String token = loginAsAdmin();
30-
// StreamingResponseBody 采用异步派发:先获取 MvcResult,再通过 asyncDispatch 触发真实写入
63+
64+
// 第一步:发起请求,确认异步处理已启动(StreamingResponseBody 异步写入)
3165
MvcResult mvcResult = mockMvc.perform(post("/openai/responses/stream")
3266
.header("satoken", token)
3367
.contentType(MediaType.APPLICATION_JSON)
@@ -39,13 +73,18 @@ void streamReturnsSseEventsForAuthenticatedUser() throws Exception {
3973
.andExpect(request().asyncStarted())
4074
.andReturn();
4175

42-
// relayEvents() 将 OpenAI choices[0].delta.content 转换为 Vercel Stream 格式 "0:\"hello\"\n"
76+
// 第二步:触发异步派发,断言响应体包含 Vercel Stream 前导符 "0:" 和内容 "hello"
77+
// relayEvents() 从 choices[0].delta.content 提取文本,转换为 0:"<text>"\n 格式
4378
mockMvc.perform(asyncDispatch(mvcResult))
4479
.andExpect(status().isOk())
4580
.andExpect(content().string(org.hamcrest.Matchers.containsString("0:")))
4681
.andExpect(content().string(org.hamcrest.Matchers.containsString("hello")));
4782
}
4883

84+
/**
85+
* 未携带 token 访问流式接口,SaInterceptor 在请求到达控制器前即触发 NOT_TOKEN 异常,
86+
* 返回 401 + "未提供 Token",不会进入异步处理流程。
87+
*/
4988
@Test
5089
void streamRejectsAnonymousRequest() throws Exception {
5190
mockMvc.perform(post("/openai/responses/stream")
@@ -60,6 +99,10 @@ void streamRejectsAnonymousRequest() throws Exception {
6099
.andExpect(jsonPath("$.message").value("未提供 Token"));
61100
}
62101

102+
/**
103+
* messages 为空数组时,@NotEmpty 校验失败,返回 400 + 字段级错误消息。
104+
* 旧测试发送 {"message": ""} 并期望 "message: 消息不能为空",已按新 DTO 结构更新。
105+
*/
63106
@Test
64107
void streamValidatesEmptyMessages() throws Exception {
65108
String token = loginAsAdmin();
@@ -81,7 +124,8 @@ void streamValidatesEmptyMessages() throws Exception {
81124
static class OpenAiTestConfiguration {
82125

83126
/**
84-
* 提供一个稳定的测试桩网关,避免控制器测试依赖真实 OpenAI 或 Mockito。
127+
* 替换真实 {@link OpenAiStreamGateway},避免集成测试依赖外部 OpenAI 服务或 Mockito。
128+
* {@code @Primary} 确保此 Bean 在有多个同类型 Bean 时优先被注入。
85129
*/
86130
@Bean
87131
@Primary
@@ -92,16 +136,19 @@ OpenAiStreamGateway openAiStreamGateway() {
92136

93137
private static final class StubOpenAiStreamGateway implements OpenAiStreamGateway {
94138

95-
/**
96-
* 测试环境下跳过外部 OpenAI 配置校验。
97-
*/
139+
/** 测试环境无需校验 OpenAI 配置(apiKey 等),直接放行。 */
98140
@Override
99141
public void validateConfiguration(OpenAiStreamRequest request) {
100142
}
101143

102144
/**
103-
* 返回固定的 OpenAI SSE 格式事件流(与真实 API 格式保持一致)。
104-
* relayEvents() 提取 choices[0].delta.content 写入 Vercel Stream 格式。
145+
* 返回一条符合 OpenAI SSE 协议格式的固定响应,供 {@code relayEvents()} 解析。
146+
*
147+
* <p>{@code relayEvents()} 从 {@code choices[0].delta.content} 提取文本,
148+
* 转换为 {@code 0:"hello"\n} 写入输出流。
149+
* 旧版 stub 使用 {@code {"type":"...","delta":"..."}} 格式,
150+
* 与 OpenAI 实际格式不符,{@code relayEvents()} 无法找到 {@code choices} 节点,
151+
* 导致输出流为空,asyncDispatch 后 content 断言失败。</p>
105152
*/
106153
@Override
107154
public InputStream openStream(OpenAiStreamRequest request) {

src/test/java/com/involutionhell/backend/support/AbstractWebIntegrationTest.java

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,39 @@
1313
import org.springframework.test.web.servlet.MockMvc;
1414
import org.springframework.test.web.servlet.MvcResult;
1515

16-
// 显式指定 H2 内存库配置,优先级高于环境变量(如 SPRING_DATASOURCE_URL),
17-
// 确保集成测试始终使用 H2 而非生产 PostgreSQL 连接。
16+
/**
17+
* Web 集成测试公共基类,提供 MockMvc 和预置登录辅助方法。
18+
*
19+
* <h3>为什么在 @SpringBootTest 中显式指定数据源属性?</h3>
20+
* <p>Spring Boot 属性优先级从高到低依次为:
21+
* <ol>
22+
* <li>{@code @SpringBootTest(properties = {...})} — 最高</li>
23+
* <li>操作系统环境变量(如 {@code SPRING_DATASOURCE_URL})</li>
24+
* <li>{@code application-test.properties} 等 Profile 配置文件 — 最低</li>
25+
* </ol>
26+
* 测试服务器上的 {@code SPRING_DATASOURCE_URL} 环境变量指向生产 Neon PostgreSQL,
27+
* 其优先级高于 {@code application-test.properties} 中的 H2 配置,导致集成测试
28+
* 尝试连接 PostgreSQL,上下文启动失败,所有 Web 集成测试报错。
29+
* 通过在 {@code @SpringBootTest(properties)} 中覆盖数据源属性,可绕过环境变量,
30+
* 确保测试始终使用 H2 内存库。</p>
31+
*
32+
* <h3>为什么还要覆盖 JustAuth redirect-uri?</h3>
33+
* <p>JustAuth 内部使用 Apache Commons {@code UrlValidator} 校验 redirect-uri 格式。
34+
* 默认情况下,{@code UrlValidator} 拒绝 {@code localhost} 域名(视为非法 URL),
35+
* 而 {@code application.properties} 中的默认值是 {@code http://localhost:3000/...}。
36+
* 若不覆盖,{@code OAuthController} 在构建 {@code AuthGithubRequest} 时会立即抛出
37+
* {@code AuthException: Illegal redirect uri},导致所有 OAuth 相关集成测试以 500 失败。
38+
* 测试环境仅需要一个格式合法的占位 URL,不会发起任何真实网络请求。</p>
39+
*/
1840
@SpringBootTest(properties = {
41+
// --- 数据源:覆盖 SPRING_DATASOURCE_URL 环境变量,强制使用 H2 内存库 ---
1942
"spring.datasource.url=jdbc:h2:mem:backend;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE",
2043
"spring.datasource.username=sa",
2144
"spring.datasource.password=",
2245
"spring.datasource.driver-class-name=org.h2.Driver",
2346
"spring.sql.init.mode=always",
2447
"spring.sql.init.schema-locations=classpath:test-schema.sql",
25-
// JustAuth 使用 Apache Commons UrlValidator 校验 redirect-uri,默认拒绝 localhost。
26-
// 测试环境使用合法格式的占位 URL,不影响实际网络请求。
48+
// --- JustAuth:使用合法格式的占位 redirect-uri,规避 UrlValidator localhost 限制 ---
2749
"justauth.type.github.redirect-uri=https://example.com/api/auth/callback/github",
2850
"justauth.type.github.client-id=test-client-id",
2951
"justauth.type.github.client-secret=test-client-secret"
@@ -36,7 +58,7 @@ public abstract class AbstractWebIntegrationTest {
3658
protected MockMvc mockMvc;
3759

3860
/**
39-
* 使用指定账号登录并提取 Sa-Token 值。
61+
* 使用指定账号登录并提取 Sa-Token 值,供子类测试方法携带 token 调用受保护接口
4062
*/
4163
protected String loginAndGetToken(String username, String password) throws Exception {
4264
MvcResult result = mockMvc.perform(post("/auth/login")
@@ -54,23 +76,17 @@ protected String loginAndGetToken(String username, String password) throws Excep
5476
return JsonPath.read(result.getResponse().getContentAsString(), "$.data.tokenValue");
5577
}
5678

57-
/**
58-
* 以管理员身份登录。
59-
*/
79+
/** 以管理员身份登录(拥有全部权限:user:profile:read, user:center:read, user:center:manage)。 */
6080
protected String loginAsAdmin() throws Exception {
6181
return loginAndGetToken("admin", "Admin@123456");
6282
}
6383

64-
/**
65-
* 以普通用户身份登录。
66-
*/
84+
/** 以普通用户身份登录(仅有 user:profile:read 权限,无法访问用户中心管理接口)。 */
6785
protected String loginAsAlice() throws Exception {
6886
return loginAndGetToken("alice", "Alice@123456");
6987
}
7088

71-
/**
72-
* 以审计员身份登录。
73-
*/
89+
/** 以审计员身份登录(拥有 user:profile:read, user:center:read,无 user:center:manage)。 */
7490
protected String loginAsAuditor() throws Exception {
7591
return loginAndGetToken("auditor", "Audit@123456");
7692
}

src/test/java/com/involutionhell/backend/usercenter/controller/AuthControllerIntegrationTests.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,26 @@
99
import org.junit.jupiter.api.Test;
1010
import org.springframework.http.MediaType;
1111

12+
/**
13+
* AuthController 集成测试(账号密码登录、退出、当前用户查询)。
14+
*
15+
* <h3>历史测试失败原因(已修复)</h3>
16+
* <p>修复前,所有测试都报 "Driver claims to not accept jdbcUrl, jdbc:postgresql://...",
17+
* 根本原因是 {@code SPRING_DATASOURCE_URL} 环境变量覆盖了 {@code application-test.properties}
18+
* 中的 H2 配置,已通过 {@link com.involutionhell.backend.support.AbstractWebIntegrationTest}
19+
* 的 {@code @SpringBootTest(properties)} 解决。</p>
20+
*
21+
* <h3>匿名请求错误消息的变化("未登录..." → "未提供 Token")</h3>
22+
* <p>旧版测试断言 {@code "未登录或登录状态已失效"},此消息来自早期 GlobalExceptionHandler
23+
* 使用通用文案的版本。当前 GlobalExceptionHandler 对 {@code NotLoginException} 按场景值细分:
24+
* <ul>
25+
* <li>{@code NOT_TOKEN}(完全未携带 token)→ "未提供 Token"</li>
26+
* <li>{@code INVALID_TOKEN}(token 格式非法)→ "Token 无效"</li>
27+
* <li>{@code TOKEN_TIMEOUT} → "Token 已过期"</li>
28+
* <li>…</li>
29+
* </ul>
30+
* 匿名请求属于 {@code NOT_TOKEN} 场景,故正确消息为 "未提供 Token"。</p>
31+
*/
1232
class AuthControllerIntegrationTests extends AbstractWebIntegrationTest {
1333

1434
@Test
@@ -70,6 +90,10 @@ void meReturnsCurrentUserWhenLoggedIn() throws Exception {
7090
.andExpect(jsonPath("$.data.permissions[0]").isNotEmpty());
7191
}
7292

93+
/**
94+
* 未携带任何 token 访问受保护接口,Sa-Token 抛出 NOT_TOKEN 场景的 NotLoginException,
95+
* GlobalExceptionHandler 将其映射为 "未提供 Token"(而非旧版通用文案"未登录或登录状态已失效")。
96+
*/
7397
@Test
7498
void meRejectsAnonymousRequest() throws Exception {
7599
mockMvc.perform(get("/auth/me"))
@@ -87,11 +111,16 @@ void logoutSucceedsAndMakesTokenInvalid() throws Exception {
87111
.andExpect(jsonPath("$.success").value(true))
88112
.andExpect(jsonPath("$.message").value("退出成功"));
89113

114+
// 退出后原 token 应失效,再次访问 /me 返回 401
90115
mockMvc.perform(get("/auth/me").header("satoken", token))
91116
.andExpect(status().isUnauthorized())
92117
.andExpect(jsonPath("$.success").value(false));
93118
}
94119

120+
/**
121+
* 匿名 POST /auth/logout,同样属于 NOT_TOKEN 场景,期望 "未提供 Token"。
122+
* 旧版测试使用通用消息,已更新为当前 GlobalExceptionHandler 的实际输出。
123+
*/
95124
@Test
96125
void logoutRejectsAnonymousRequest() throws Exception {
97126
mockMvc.perform(post("/auth/logout"))

0 commit comments

Comments
 (0)