SecurityAccessTest.java 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. package com.xuekairui.gateway;
  2. import org.junit.jupiter.api.DisplayName;
  3. import org.junit.jupiter.api.Nested;
  4. import org.junit.jupiter.api.Test;
  5. import org.springframework.http.MediaType;
  6. import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
  7. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
  8. /**
  9. * 安全访问控制测试
  10. * 验证 Security 白名单和鉴权逻辑
  11. */
  12. @DisplayName("安全访问控制测试")
  13. class SecurityAccessTest extends GatewayBaseTest {
  14. @Nested
  15. @DisplayName("公开接口(无需 Token)")
  16. class PublicEndpoints {
  17. @Test
  18. @DisplayName("GET /api/membership/levels - 等级列表应公开访问")
  19. void levelListShouldBePublic() throws Exception {
  20. mockMvc.perform(get("/api/membership/levels"))
  21. .andExpect(status().isOk())
  22. .andExpect(jsonPath("$.code").value(200));
  23. }
  24. @Test
  25. @DisplayName("GET /api/membership/levels/{id} - 等级详情公开访问(/api/membership/levels/* 在白名单中)")
  26. void levelDetailShouldRequireAuth() throws Exception {
  27. mockMvc.perform(get("/api/membership/levels/1"))
  28. .andExpect(status().isOk());
  29. }
  30. @Test
  31. @DisplayName("POST /api/auth/sms/send - 发送验证码应公开访问")
  32. void sendCodeShouldBePublic() throws Exception {
  33. mockMvc.perform(post("/api/auth/sms/send")
  34. .contentType(MediaType.APPLICATION_JSON)
  35. .content("{\"phone\":\"13800138000\",\"scene\":\"login\"}"))
  36. .andExpect(status().isOk());
  37. }
  38. @Test
  39. @DisplayName("POST /api/auth/sms/login - 登录接口应公开访问")
  40. void smsLoginShouldBePublic() throws Exception {
  41. mockMvc.perform(post("/api/auth/sms/login")
  42. .contentType(MediaType.APPLICATION_JSON)
  43. .content("{\"phone\":\"13800138000\",\"code\":\"123456\"}"))
  44. .andExpect(status().isOk());
  45. }
  46. @Test
  47. @DisplayName("GET /api/auth/wechat/url - 微信登录URL应公开访问")
  48. void wechatUrlShouldBePublic() throws Exception {
  49. mockMvc.perform(get("/api/auth/wechat/url"))
  50. .andExpect(status().isOk());
  51. }
  52. @Test
  53. @DisplayName("GET /api/auth/wechat/callback - 微信回调应公开访问")
  54. void wechatCallbackShouldBePublic() throws Exception {
  55. mockMvc.perform(get("/api/auth/wechat/callback")
  56. .param("code", "test")
  57. .param("state", "login"))
  58. .andExpect(status().isOk());
  59. }
  60. @Test
  61. @DisplayName("GET /api/invite/page/{code} - 邀请落地页应公开访问")
  62. void invitePageShouldBePublic() throws Exception {
  63. mockMvc.perform(get("/api/invite/page/TESTCODE"))
  64. .andExpect(status().isOk());
  65. }
  66. @Test
  67. @DisplayName("POST /api/invite/click/{code} - 点击追踪应公开访问")
  68. void inviteClickShouldBePublic() throws Exception {
  69. mockMvc.perform(post("/api/invite/click/TESTCODE"))
  70. .andExpect(status().isOk());
  71. }
  72. @Test
  73. @DisplayName("GET /api/invite/page/{code}?channel=app - 多渠道落地页应公开访问")
  74. void invitePageMultiChannelShouldBePublic() throws Exception {
  75. mockMvc.perform(get("/api/invite/page/TESTCODE?channel=app"))
  76. .andExpect(status().isOk());
  77. }
  78. @Test
  79. @DisplayName("GET /api/trial-quota/active - 体验配额应公开访问")
  80. void trialQuotaActiveShouldBePublic() throws Exception {
  81. mockMvc.perform(get("/api/trial-quota/active"))
  82. .andExpect(status().isOk());
  83. }
  84. @Test
  85. @DisplayName("GET /api/platform-config/enabled - 平台配置应公开访问")
  86. void platformConfigShouldBePublic() throws Exception {
  87. mockMvc.perform(get("/api/platform-config/enabled"))
  88. .andExpect(status().isOk());
  89. }
  90. }
  91. @Nested
  92. @DisplayName("受保护接口 - 用户级(需要 Token)")
  93. class ProtectedEndpoints {
  94. @Test
  95. @DisplayName("GET /api/membership/my - 无Token应返回401")
  96. void myLevelWithoutTokenShouldReturn401() throws Exception {
  97. mockMvc.perform(get("/api/membership/my"))
  98. .andExpect(status().isUnauthorized());
  99. }
  100. @Test
  101. @DisplayName("GET /api/crawler/status - 无Token应返回401")
  102. void crawlerStatusWithoutTokenShouldReturn401() throws Exception {
  103. mockMvc.perform(get("/api/crawler/status"))
  104. .andExpect(status().isUnauthorized());
  105. }
  106. @Test
  107. @DisplayName("POST /api/crawler/consume - 无Token应返回401")
  108. void crawlerConsumeWithoutTokenShouldReturn401() throws Exception {
  109. mockMvc.perform(post("/api/crawler/consume")
  110. .contentType(MediaType.APPLICATION_JSON)
  111. .content("{\"count\":1}"))
  112. .andExpect(status().isUnauthorized());
  113. }
  114. @Test
  115. @DisplayName("GET /api/crawler/logs - 无Token应返回401")
  116. void crawlerLogsWithoutTokenShouldReturn401() throws Exception {
  117. mockMvc.perform(get("/api/crawler/logs"))
  118. .andExpect(status().isUnauthorized());
  119. }
  120. @Test
  121. @DisplayName("GET /api/coupon/my - 无Token应返回401")
  122. void myCouponsWithoutTokenShouldReturn401() throws Exception {
  123. mockMvc.perform(get("/api/coupon/my"))
  124. .andExpect(status().isUnauthorized());
  125. }
  126. @Test
  127. @DisplayName("GET /api/coupon/my/available - 无Token应返回401")
  128. void availableCouponsWithoutTokenShouldReturn401() throws Exception {
  129. mockMvc.perform(get("/api/coupon/my/available"))
  130. .andExpect(status().isUnauthorized());
  131. }
  132. @Test
  133. @DisplayName("POST /api/coupon/issue - 无Token应返回401")
  134. void issueCouponWithoutTokenShouldReturn401() throws Exception {
  135. mockMvc.perform(post("/api/coupon/issue")
  136. .contentType(MediaType.APPLICATION_JSON)
  137. .content("{}"))
  138. .andExpect(status().isUnauthorized());
  139. }
  140. @Test
  141. @DisplayName("GET /api/auth/user/info - 无Token应返回401")
  142. void userInfoWithoutTokenShouldReturn401() throws Exception {
  143. mockMvc.perform(get("/api/auth/user/info"))
  144. .andExpect(status().isUnauthorized());
  145. }
  146. @Test
  147. @DisplayName("GET /api/invite/code - 无Token应返回401")
  148. void inviteCodeWithoutTokenShouldReturn401() throws Exception {
  149. mockMvc.perform(get("/api/invite/code"))
  150. .andExpect(status().isUnauthorized());
  151. }
  152. @Test
  153. @DisplayName("GET /api/invite/stats - 无Token应返回401")
  154. void inviteStatsWithoutTokenShouldReturn401() throws Exception {
  155. mockMvc.perform(get("/api/invite/stats"))
  156. .andExpect(status().isUnauthorized());
  157. }
  158. @Test
  159. @DisplayName("GET /api/invite/rewards - 无Token应返回401")
  160. void inviteRewardsWithoutTokenShouldReturn401() throws Exception {
  161. mockMvc.perform(get("/api/invite/rewards"))
  162. .andExpect(status().isUnauthorized());
  163. }
  164. @Test
  165. @DisplayName("GET /api/invite/extra-quota - 无Token应返回401")
  166. void extraQuotaWithoutTokenShouldReturn401() throws Exception {
  167. mockMvc.perform(get("/api/invite/extra-quota"))
  168. .andExpect(status().isUnauthorized());
  169. }
  170. }
  171. @Nested
  172. @DisplayName("受保护接口 - 管理级(需要 Token)")
  173. class AdminEndpoints {
  174. @Test
  175. @DisplayName("GET /api/admin/invite/config - 无Token应返回401")
  176. void adminInviteConfigWithoutTokenShouldReturn401() throws Exception {
  177. mockMvc.perform(get("/api/admin/invite/config"))
  178. .andExpect(status().isUnauthorized());
  179. }
  180. @Test
  181. @DisplayName("PUT /api/admin/invite/config - 无Token应返回401")
  182. void adminUpdateInviteConfigWithoutTokenShouldReturn401() throws Exception {
  183. mockMvc.perform(put("/api/admin/invite/config")
  184. .contentType(MediaType.APPLICATION_JSON)
  185. .content("{}"))
  186. .andExpect(status().isUnauthorized());
  187. }
  188. @Test
  189. @DisplayName("POST /api/admin/crawler/grant - 无Token应返回401")
  190. void adminGrantCrawlerWithoutTokenShouldReturn401() throws Exception {
  191. mockMvc.perform(post("/api/admin/crawler/grant")
  192. .contentType(MediaType.APPLICATION_JSON)
  193. .content("{\"userId\":1,\"quotaCount\":50}"))
  194. .andExpect(status().isUnauthorized());
  195. }
  196. @Test
  197. @DisplayName("GET /api/admin/crawler/grants - 无Token应返回401")
  198. void adminListGrantsWithoutTokenShouldReturn401() throws Exception {
  199. mockMvc.perform(get("/api/admin/crawler/grants"))
  200. .andExpect(status().isUnauthorized());
  201. }
  202. @Test
  203. @DisplayName("GET /api/admin/invite/rewards - 无Token应返回401")
  204. void adminInviteRewardsWithoutTokenShouldReturn401() throws Exception {
  205. mockMvc.perform(get("/api/admin/invite/rewards"))
  206. .andExpect(status().isUnauthorized());
  207. }
  208. @Test
  209. @DisplayName("POST /api/admin/membership/grant - 无Token应返回401")
  210. void setUserLevelWithoutTokenShouldReturn401() throws Exception {
  211. mockMvc.perform(post("/api/admin/membership/grant")
  212. .contentType(MediaType.APPLICATION_JSON)
  213. .content("{\"userId\":1,\"level\":\"PRO\"}"))
  214. .andExpect(status().isUnauthorized());
  215. }
  216. @Test
  217. @DisplayName("GET /api/admin/trial-quota/configs - 无Token应返回401")
  218. void adminTrialQuotaConfigsWithoutTokenShouldReturn401() throws Exception {
  219. mockMvc.perform(get("/api/admin/trial-quota/configs"))
  220. .andExpect(status().isUnauthorized());
  221. }
  222. @Test
  223. @DisplayName("POST /api/admin/trial-quota/config - 无Token应返回401")
  224. void adminCreateTrialQuotaConfigWithoutTokenShouldReturn401() throws Exception {
  225. mockMvc.perform(post("/api/admin/trial-quota/config")
  226. .contentType(MediaType.APPLICATION_JSON)
  227. .content("{}"))
  228. .andExpect(status().isUnauthorized());
  229. }
  230. @Test
  231. @DisplayName("PUT /api/admin/trial-quota/config/{id} - 无Token应返回401")
  232. void adminUpdateTrialQuotaConfigWithoutTokenShouldReturn401() throws Exception {
  233. mockMvc.perform(put("/api/admin/trial-quota/config/1")
  234. .contentType(MediaType.APPLICATION_JSON)
  235. .content("{}"))
  236. .andExpect(status().isUnauthorized());
  237. }
  238. @Test
  239. @DisplayName("DELETE /api/admin/trial-quota/config/{id} - 无Token应返回401")
  240. void adminDeleteTrialQuotaConfigWithoutTokenShouldReturn401() throws Exception {
  241. mockMvc.perform(delete("/api/admin/trial-quota/config/1"))
  242. .andExpect(status().isUnauthorized());
  243. }
  244. @Test
  245. @DisplayName("GET /api/admin/audit-log - 无Token应返回401")
  246. void adminAuditLogWithoutTokenShouldReturn401() throws Exception {
  247. mockMvc.perform(get("/api/admin/audit-log"))
  248. .andExpect(status().isUnauthorized());
  249. }
  250. @Test
  251. @DisplayName("GET /api/admin/audit-log/sensitive - 无Token应返回401")
  252. void adminSensitiveLogsWithoutTokenShouldReturn401() throws Exception {
  253. mockMvc.perform(get("/api/admin/audit-log/sensitive"))
  254. .andExpect(status().isUnauthorized());
  255. }
  256. @Test
  257. @DisplayName("GET /api/admin/audit-log/user/{userId} - 无Token应返回401")
  258. void adminUserLogsWithoutTokenShouldReturn401() throws Exception {
  259. mockMvc.perform(get("/api/admin/audit-log/user/1"))
  260. .andExpect(status().isUnauthorized());
  261. }
  262. @Test
  263. @DisplayName("GET /api/admin/audit-log/target/{type}/{id} - 无Token应返回401")
  264. void adminTargetHistoryWithoutTokenShouldReturn401() throws Exception {
  265. mockMvc.perform(get("/api/admin/audit-log/target/USER/1"))
  266. .andExpect(status().isUnauthorized());
  267. }
  268. @Test
  269. @DisplayName("GET /api/admin/audit-log/stats - 无Token应返回401")
  270. void adminStatsWithoutTokenShouldReturn401() throws Exception {
  271. mockMvc.perform(get("/api/admin/audit-log/stats?module=USER"))
  272. .andExpect(status().isUnauthorized());
  273. }
  274. @Test
  275. @DisplayName("GET /api/admin/license/review - 无Token应返回401")
  276. void adminLicenseReviewWithoutTokenShouldReturn401() throws Exception {
  277. mockMvc.perform(get("/api/admin/license/review"))
  278. .andExpect(status().isUnauthorized());
  279. }
  280. @Test
  281. @DisplayName("POST /api/admin/license/review - 无Token应返回401")
  282. void adminReviewLicenseWithoutTokenShouldReturn401() throws Exception {
  283. mockMvc.perform(post("/api/admin/license/review")
  284. .contentType(MediaType.APPLICATION_JSON)
  285. .content("{}"))
  286. .andExpect(status().isUnauthorized());
  287. }
  288. }
  289. @Nested
  290. @DisplayName("Token 有效性验证")
  291. class TokenValidation {
  292. @Test
  293. @DisplayName("GET /api/membership/my - 携带有效Token应正常返回")
  294. void myLevelWithValidTokenShouldSucceed() throws Exception {
  295. String token = generateAccessToken(1L, "13800138000");
  296. mockMvc.perform(get("/api/membership/my")
  297. .header("Authorization", "Bearer " + token))
  298. .andExpect(status().isOk())
  299. .andExpect(jsonPath("$.code").value(200));
  300. }
  301. @Test
  302. @DisplayName("GET /api/crawler/status - 携带有效Token应正常返回或业务错误")
  303. void crawlerStatusWithValidTokenShouldSucceed() throws Exception {
  304. String token = generateAccessToken(1L, "13800138000");
  305. mockMvc.perform(get("/api/crawler/status")
  306. .header("Authorization", "Bearer " + token))
  307. .andExpect(status().isOk());
  308. // 可能返回200(配额正常)或200+业务错误码(配额耗尽),但不应401
  309. }
  310. @Test
  311. @DisplayName("携带无效Token应返回401")
  312. void invalidTokenShouldReturn401() throws Exception {
  313. mockMvc.perform(get("/api/membership/my")
  314. .header("Authorization", "Bearer invalid.token.here"))
  315. .andExpect(status().isUnauthorized());
  316. }
  317. @Test
  318. @DisplayName("携带空Token应返回401")
  319. void emptyTokenShouldReturn401() throws Exception {
  320. mockMvc.perform(get("/api/membership/my")
  321. .header("Authorization", "Bearer "))
  322. .andExpect(status().isUnauthorized());
  323. }
  324. @Test
  325. @DisplayName("不同用户的Token应各自有效")
  326. void differentUserTokensShouldWork() throws Exception {
  327. String token1 = generateAccessToken(1L, "13800138000");
  328. String token2 = generateAccessToken(2L, "13900139000");
  329. mockMvc.perform(get("/api/membership/my")
  330. .header("Authorization", "Bearer " + token1))
  331. .andExpect(status().isOk());
  332. mockMvc.perform(get("/api/membership/my")
  333. .header("Authorization", "Bearer " + token2))
  334. .andExpect(status().isOk());
  335. }
  336. }
  337. @Nested
  338. @DisplayName("HTTP 方法验证")
  339. class HttpMethodValidation {
  340. @Test
  341. @DisplayName("GET /api/auth/sms/send - 应只支持POST")
  342. void sendCode_getMethodShouldFail() throws Exception {
  343. mockMvc.perform(get("/api/auth/sms/send"))
  344. .andExpect(status().is5xxServerError());
  345. }
  346. @Test
  347. @DisplayName("DELETE /api/membership/levels - 不支持的方法")
  348. void levelList_deleteMethodShouldReturn405() throws Exception {
  349. mockMvc.perform(delete("/api/membership/levels"))
  350. .andExpect(status().is5xxServerError());
  351. }
  352. }
  353. }