AdminControllerTest.java 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. package com.xuekairui.gateway;
  2. import org.junit.jupiter.api.*;
  3. import org.springframework.http.MediaType;
  4. import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
  5. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
  6. /**
  7. * 运营管理接口测试
  8. * 覆盖:邀请配置管理、爬虫次数手动发放、发放记录查询
  9. */
  10. @DisplayName("运营管理接口测试")
  11. @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
  12. class AdminControllerTest extends GatewayBaseTest {
  13. private String adminToken() {
  14. return generateAccessToken(1L, "13800138000");
  15. }
  16. // ==================== 邀请配置管理 ====================
  17. @Test
  18. @Order(1)
  19. @DisplayName("GET /api/admin/invite/config - 应返回邀请配置")
  20. void getInviteConfig_shouldReturnConfig() throws Exception {
  21. mockMvc.perform(get("/api/admin/invite/config")
  22. .header("Authorization", "Bearer " + adminToken()))
  23. .andExpect(status().isOk())
  24. .andExpect(jsonPath("$.code").value(200))
  25. .andExpect(jsonPath("$.data.rewardCrawlerCount").isNumber())
  26. .andExpect(jsonPath("$.data.maxDailyReward").isNumber())
  27. .andExpect(jsonPath("$.data.inviteCodeExpireDays").isNumber())
  28. .andExpect(jsonPath("$.data.maxInvitePerDay").isNumber());
  29. }
  30. @Test
  31. @Order(2)
  32. @DisplayName("GET /api/admin/invite/config - 无Token应返回401")
  33. void getInviteConfig_withoutToken_shouldReturn401() throws Exception {
  34. mockMvc.perform(get("/api/admin/invite/config"))
  35. .andExpect(status().isUnauthorized());
  36. }
  37. @Test
  38. @Order(3)
  39. @DisplayName("GET /api/admin/invite/config - 配置应包含多渠道字段")
  40. void getInviteConfig_shouldContainMultiChannelFields() throws Exception {
  41. mockMvc.perform(get("/api/admin/invite/config")
  42. .header("Authorization", "Bearer " + adminToken()))
  43. .andExpect(status().isOk())
  44. .andExpect(jsonPath("$.data.status").isNumber());
  45. }
  46. @Test
  47. @Order(4)
  48. @DisplayName("PUT /api/admin/invite/config - 更新邀请配置")
  49. void updateInviteConfig_shouldSucceed() throws Exception {
  50. String body = """
  51. {
  52. "rewardCrawlerCount": 5,
  53. "maxDailyReward": 50,
  54. "inviteCodeExpireDays": 30,
  55. "maxInvitePerDay": 10,
  56. "landingTitle": "测试标题",
  57. "appName": "智价云(药店版)"
  58. }
  59. """;
  60. mockMvc.perform(put("/api/admin/invite/config")
  61. .header("Authorization", "Bearer " + adminToken())
  62. .contentType(MediaType.APPLICATION_JSON)
  63. .content(body))
  64. .andExpect(status().isOk())
  65. .andExpect(jsonPath("$.code").value(200));
  66. }
  67. @Test
  68. @Order(5)
  69. @DisplayName("PUT /api/admin/invite/config - 更新含多渠道配置")
  70. void updateInviteConfig_withChannels_shouldSucceed() throws Exception {
  71. String body = """
  72. {
  73. "rewardCrawlerCount": 5,
  74. "maxDailyReward": 50,
  75. "inviteCodeExpireDays": 30,
  76. "maxInvitePerDay": 10,
  77. "appDownloadUrl": "https://download.example.com/app.exe",
  78. "miniappPath": "/pages/download/index",
  79. "miniappAppId": "wx1234567890",
  80. "wechatRedirectUrl": "https://mp.weixin.qq.com/xxx",
  81. "dingtalkAppId": "dingxxx",
  82. "feishuAppId": "feishuxxx",
  83. "landingTitle": "邀请你加入",
  84. "landingDesc": "高效数据采集",
  85. "appName": "智价云(药店版)"
  86. }
  87. """;
  88. mockMvc.perform(put("/api/admin/invite/config")
  89. .header("Authorization", "Bearer " + adminToken())
  90. .contentType(MediaType.APPLICATION_JSON)
  91. .content(body))
  92. .andExpect(status().isOk())
  93. .andExpect(jsonPath("$.code").value(200));
  94. }
  95. @Test
  96. @Order(6)
  97. @DisplayName("PUT /api/admin/invite/config - 无Token应返回401")
  98. void updateInviteConfig_withoutToken_shouldReturn401() throws Exception {
  99. mockMvc.perform(put("/api/admin/invite/config")
  100. .contentType(MediaType.APPLICATION_JSON)
  101. .content("{}"))
  102. .andExpect(status().isUnauthorized());
  103. }
  104. @Test
  105. @Order(7)
  106. @DisplayName("GET /api/admin/invite/configs - 应返回分页配置列表")
  107. void listInviteConfigs_shouldReturnPage() throws Exception {
  108. mockMvc.perform(get("/api/admin/invite/configs")
  109. .header("Authorization", "Bearer " + adminToken()))
  110. .andExpect(status().isOk())
  111. .andExpect(jsonPath("$.code").value(200))
  112. .andExpect(jsonPath("$.data.records").isArray())
  113. .andExpect(jsonPath("$.data.total").isNumber())
  114. .andExpect(jsonPath("$.data.current").value(1))
  115. .andExpect(jsonPath("$.data.size").value(20));
  116. }
  117. @Test
  118. @Order(8)
  119. @DisplayName("GET /api/admin/invite/config/{id} - 根据ID获取配置")
  120. void getInviteConfigById_shouldReturnConfig() throws Exception {
  121. mockMvc.perform(get("/api/admin/invite/config/1")
  122. .header("Authorization", "Bearer " + adminToken()))
  123. .andExpect(status().isOk())
  124. .andExpect(jsonPath("$.code").value(200))
  125. .andExpect(jsonPath("$.data.id").value(1))
  126. .andExpect(jsonPath("$.data.rewardCrawlerCount").isNumber());
  127. }
  128. @Test
  129. @Order(9)
  130. @DisplayName("GET /api/admin/invite/config/{id} - 不存在应返回错误")
  131. void getInviteConfigById_notFound_shouldReturnError() throws Exception {
  132. mockMvc.perform(get("/api/admin/invite/config/99999")
  133. .header("Authorization", "Bearer " + adminToken()))
  134. .andExpect(status().isOk())
  135. .andExpect(jsonPath("$.code").isNumber());
  136. }
  137. @Test
  138. @Order(10)
  139. @DisplayName("POST /api/admin/invite/config - 创建新配置")
  140. void createInviteConfig_shouldSucceed() throws Exception {
  141. String body = """
  142. {
  143. "rewardCrawlerCount": 3,
  144. "rewardType": "MEMBERSHIP",
  145. "rewardMonths": 2,
  146. "maxDailyReward": 30,
  147. "inviteCodeExpireDays": 15,
  148. "maxInvitePerDay": 5,
  149. "maxTotalInvites": 20,
  150. "landingTitle": "新配置标题",
  151. "appName": "智价云(药店版)"
  152. }
  153. """;
  154. mockMvc.perform(post("/api/admin/invite/config")
  155. .header("Authorization", "Bearer " + adminToken())
  156. .contentType(MediaType.APPLICATION_JSON)
  157. .content(body))
  158. .andExpect(status().isOk())
  159. .andExpect(jsonPath("$.code").value(200))
  160. .andExpect(jsonPath("$.data.id").isNumber())
  161. .andExpect(jsonPath("$.data.landingTitle").value("新配置标题"));
  162. }
  163. @Test
  164. @Order(11)
  165. @DisplayName("PUT /api/admin/invite/config/{id} - 更新指定ID的配置")
  166. void updateInviteConfigById_shouldSucceed() throws Exception {
  167. String body = """
  168. {
  169. "rewardMonths": 3,
  170. "landingTitle": "更新后的标题"
  171. }
  172. """;
  173. mockMvc.perform(put("/api/admin/invite/config/1")
  174. .header("Authorization", "Bearer " + adminToken())
  175. .contentType(MediaType.APPLICATION_JSON)
  176. .content(body))
  177. .andExpect(status().isOk())
  178. .andExpect(jsonPath("$.code").value(200));
  179. }
  180. @Test
  181. @Order(12)
  182. @DisplayName("PUT /api/admin/invite/config/{id} - 不存在应返回错误")
  183. void updateInviteConfigById_notFound_shouldReturnError() throws Exception {
  184. mockMvc.perform(put("/api/admin/invite/config/99999")
  185. .header("Authorization", "Bearer " + adminToken())
  186. .contentType(MediaType.APPLICATION_JSON)
  187. .content("{\"rewardMonths\": 3}"))
  188. .andExpect(status().isOk())
  189. .andExpect(jsonPath("$.code").isNumber());
  190. }
  191. @Test
  192. @Order(13)
  193. @DisplayName("DELETE /api/admin/invite/config/{id} - 删除配置")
  194. void deleteInviteConfig_shouldSucceed() throws Exception {
  195. // 先创建一个临时配置用于删除
  196. String createBody = """
  197. {
  198. "rewardMonths": 1,
  199. "landingTitle": "待删除配置"
  200. }
  201. """;
  202. String createResult = mockMvc.perform(post("/api/admin/invite/config")
  203. .header("Authorization", "Bearer " + adminToken())
  204. .contentType(MediaType.APPLICATION_JSON)
  205. .content(createBody))
  206. .andExpect(status().isOk())
  207. .andReturn().getResponse().getContentAsString();
  208. com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
  209. int newId = mapper.readTree(createResult).get("data").get("id").asInt();
  210. mockMvc.perform(delete("/api/admin/invite/config/" + newId)
  211. .header("Authorization", "Bearer " + adminToken()))
  212. .andExpect(status().isOk())
  213. .andExpect(jsonPath("$.code").value(200));
  214. // 再次查询应返回错误
  215. mockMvc.perform(get("/api/admin/invite/config/" + newId)
  216. .header("Authorization", "Bearer " + adminToken()))
  217. .andExpect(status().isOk())
  218. .andExpect(jsonPath("$.code").isNumber());
  219. }
  220. // ==================== 爬虫次数手动发放 ====================
  221. @Test
  222. @Order(10)
  223. @DisplayName("POST /api/admin/crawler/grant - 手动发放爬虫次数")
  224. void grantCrawlerQuota_shouldSucceed() throws Exception {
  225. String body = """
  226. {
  227. "userId": 1,
  228. "quotaCount": 50,
  229. "expireDays": 30,
  230. "remark": "测试发放"
  231. }
  232. """;
  233. mockMvc.perform(post("/api/admin/crawler/grant")
  234. .header("Authorization", "Bearer " + adminToken())
  235. .contentType(MediaType.APPLICATION_JSON)
  236. .content(body))
  237. .andExpect(status().isOk())
  238. .andExpect(jsonPath("$.code").value(200));
  239. }
  240. @Test
  241. @Order(11)
  242. @DisplayName("POST /api/admin/crawler/grant - 无Token应返回401")
  243. void grantCrawlerQuota_withoutToken_shouldReturn401() throws Exception {
  244. mockMvc.perform(post("/api/admin/crawler/grant")
  245. .contentType(MediaType.APPLICATION_JSON)
  246. .content("{\"userId\":1,\"quotaCount\":50}"))
  247. .andExpect(status().isUnauthorized());
  248. }
  249. @Test
  250. @Order(12)
  251. @DisplayName("GET /api/admin/crawler/grants - 应返回发放记录列表")
  252. void listGrants_shouldReturnList() throws Exception {
  253. mockMvc.perform(get("/api/admin/crawler/grants")
  254. .header("Authorization", "Bearer " + adminToken()))
  255. .andExpect(status().isOk())
  256. .andExpect(jsonPath("$.code").value(200))
  257. .andExpect(jsonPath("$.data").isArray());
  258. }
  259. @Test
  260. @Order(13)
  261. @DisplayName("GET /api/admin/crawler/grants?userId=1 - 按用户筛选")
  262. void listGrants_byUserId_shouldFilter() throws Exception {
  263. mockMvc.perform(get("/api/admin/crawler/grants?userId=1")
  264. .header("Authorization", "Bearer " + adminToken()))
  265. .andExpect(status().isOk())
  266. .andExpect(jsonPath("$.data").isArray());
  267. }
  268. @Test
  269. @Order(14)
  270. @DisplayName("GET /api/admin/crawler/grants?grantType=ADMIN - 按类型筛选")
  271. void listGrants_byGrantType_shouldFilter() throws Exception {
  272. mockMvc.perform(get("/api/admin/crawler/grants?grantType=ADMIN")
  273. .header("Authorization", "Bearer " + adminToken()))
  274. .andExpect(status().isOk())
  275. .andExpect(jsonPath("$.data").isArray());
  276. }
  277. @Test
  278. @Order(15)
  279. @DisplayName("GET /api/admin/crawler/grants?grantType=INVITE - 查看邀请奖励记录")
  280. void listGrants_inviteType_shouldReturnList() throws Exception {
  281. mockMvc.perform(get("/api/admin/crawler/grants?grantType=INVITE")
  282. .header("Authorization", "Bearer " + adminToken()))
  283. .andExpect(status().isOk())
  284. .andExpect(jsonPath("$.data").isArray());
  285. }
  286. @Test
  287. @Order(16)
  288. @DisplayName("GET /api/admin/crawler/grants?grantType=PURCHASE - 查看购买奖励记录")
  289. void listGrants_purchaseType_shouldReturnList() throws Exception {
  290. mockMvc.perform(get("/api/admin/crawler/grants?grantType=PURCHASE")
  291. .header("Authorization", "Bearer " + adminToken()))
  292. .andExpect(status().isOk())
  293. .andExpect(jsonPath("$.data").isArray());
  294. }
  295. // ==================== 邀请记录查看 ====================
  296. @Test
  297. @Order(20)
  298. @DisplayName("GET /api/admin/invite/rewards?userId=1 - 查看用户邀请记录")
  299. void listInviteRewards_byUserId_shouldReturnList() throws Exception {
  300. mockMvc.perform(get("/api/admin/invite/rewards?userId=1")
  301. .header("Authorization", "Bearer " + adminToken()))
  302. .andExpect(status().isOk())
  303. .andExpect(jsonPath("$.code").value(200))
  304. .andExpect(jsonPath("$.data").isArray());
  305. }
  306. @Test
  307. @Order(21)
  308. @DisplayName("GET /api/admin/invite/rewards - 无userId应返回空列表")
  309. void listInviteRewards_noUserId_shouldReturnEmpty() throws Exception {
  310. mockMvc.perform(get("/api/admin/invite/rewards")
  311. .header("Authorization", "Bearer " + adminToken()))
  312. .andExpect(status().isOk())
  313. .andExpect(jsonPath("$.data").isArray());
  314. }
  315. @Test
  316. @Order(22)
  317. @DisplayName("GET /api/admin/invite/rewards - 无Token应返回401")
  318. void listInviteRewards_withoutToken_shouldReturn401() throws Exception {
  319. mockMvc.perform(get("/api/admin/invite/rewards"))
  320. .andExpect(status().isUnauthorized());
  321. }
  322. // ==================== 用户爬虫详情查询(新增)====================
  323. @Test
  324. @Order(30)
  325. @DisplayName("GET /api/admin/crawler/user-detail?userId=1 - 应返回用户爬虫详情")
  326. void getCrawlerUserDetail_shouldReturnDetail() throws Exception {
  327. mockMvc.perform(get("/api/admin/crawler/user-detail?userId=1")
  328. .header("Authorization", "Bearer " + adminToken()))
  329. .andExpect(status().isOk())
  330. .andExpect(jsonPath("$.code").value(200))
  331. .andExpect(jsonPath("$.data.userId").value(1))
  332. .andExpect(jsonPath("$.data.nickname").exists())
  333. .andExpect(jsonPath("$.data.phone").exists())
  334. .andExpect(jsonPath("$.data.todayTotalUsage").isNumber())
  335. .andExpect(jsonPath("$.data.monthlyTotalUsage").isNumber())
  336. .andExpect(jsonPath("$.data.platformStats").isArray());
  337. }
  338. @Test
  339. @Order(31)
  340. @DisplayName("GET /api/admin/crawler/user-detail - 平台统计应包含各字段")
  341. void getCrawlerUserDetail_platformStats_shouldContainFields() throws Exception {
  342. mockMvc.perform(get("/api/admin/crawler/user-detail?userId=1")
  343. .header("Authorization", "Bearer " + adminToken()))
  344. .andExpect(status().isOk())
  345. .andExpect(jsonPath("$.data.platformStats").isArray())
  346. .andExpect(result -> {
  347. // 解析JSON验证平台统计
  348. String content = result.getResponse().getContentAsString();
  349. com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
  350. com.fasterxml.jackson.databind.JsonNode json = mapper.readTree(content);
  351. com.fasterxml.jackson.databind.JsonNode stats = json.get("data").get("platformStats");
  352. // 如果有平台统计数据,验证每个元素的字段
  353. if (stats.isArray() && stats.size() > 0) {
  354. for (int i = 0; i < stats.size(); i++) {
  355. var stat = stats.get(i);
  356. // 验证必填字段存在且不为null
  357. assert stat.has("platformCode") && !stat.get("platformCode").isNull() :
  358. "platformCode不能为空";
  359. assert stat.has("platformName") && !stat.get("platformName").isNull() :
  360. "platformName不能为空";
  361. assert stat.has("todayUsage") && stat.get("todayUsage").isNumber() :
  362. "todayUsage必须是数字";
  363. assert stat.has("monthlyUsage") && stat.get("monthlyUsage").isNumber() :
  364. "monthlyUsage必须是数字";
  365. assert stat.has("totalUsage") && stat.get("totalUsage").isNumber() :
  366. "totalUsage必须是数字";
  367. // 验证数值>=0
  368. assert stat.get("todayUsage").asInt() >= 0 : "todayUsage不能为负数";
  369. assert stat.get("monthlyUsage").asInt() >= 0 : "monthlyUsage不能为负数";
  370. assert stat.get("totalUsage").asInt() >= 0 : "totalUsage不能为负数";
  371. // 验证平台代码有效(已知枚举或容错显示为“其他平台”)
  372. String platformCode = stat.get("platformCode").asText();
  373. String platformName = stat.get("platformName").asText();
  374. com.xuekairui.user.entity.CrawlerPlatform enumVal =
  375. com.xuekairui.user.entity.CrawlerPlatform.findByCode(platformCode);
  376. if (enumVal != null) {
  377. assert platformName.equals(enumVal.getName()) :
  378. "平台名称不匹配: " + platformName + " vs " + enumVal.getName();
  379. } else {
  380. assert platformName.startsWith("其他平台") :
  381. "未知平台应显示'其他平台(xxx)': " + platformName;
  382. }
  383. }
  384. // 验证按总使用次数降序排列
  385. if (stats.size() > 1) {
  386. for (int i = 1; i < stats.size(); i++) {
  387. int prevTotal = stats.get(i - 1).get("totalUsage").asInt();
  388. int currTotal = stats.get(i).get("totalUsage").asInt();
  389. assert prevTotal >= currTotal :
  390. "平台统计应按总使用次数降序排列,但索引" + (i-1) + "的" + prevTotal +
  391. " < 索引" + i + "的" + currTotal;
  392. }
  393. }
  394. }
  395. });
  396. }
  397. @Test
  398. @Order(32)
  399. @DisplayName("GET /api/admin/crawler/user-detail - 手机号应脱敏")
  400. void getCrawlerUserDetail_phoneShouldBeMasked() throws Exception {
  401. mockMvc.perform(get("/api/admin/crawler/user-detail?userId=1")
  402. .header("Authorization", "Bearer " + adminToken()))
  403. .andExpect(status().isOk())
  404. .andExpect(jsonPath("$.data.phone").value("138****8000"));
  405. }
  406. @Test
  407. @Order(33)
  408. @DisplayName("GET /api/admin/crawler/user-detail - 无Token应返回401")
  409. void getCrawlerUserDetail_withoutToken_shouldReturn401() throws Exception {
  410. mockMvc.perform(get("/api/admin/crawler/user-detail?userId=1"))
  411. .andExpect(status().isUnauthorized());
  412. }
  413. @Test
  414. @Order(34)
  415. @DisplayName("GET /api/admin/crawler/user-detail?userId=999 - 不存在的用户应返回错误")
  416. void getCrawlerUserDetail_nonExistentUser_shouldReturnError() throws Exception {
  417. mockMvc.perform(get("/api/admin/crawler/user-detail?userId=999")
  418. .header("Authorization", "Bearer " + adminToken()))
  419. .andExpect(status().isOk())
  420. .andExpect(jsonPath("$.code").isNumber());
  421. }
  422. // ==================== 邀请转化统计(新增)====================
  423. @Test
  424. @Order(40)
  425. @DisplayName("GET /api/admin/invite/conversion-stats?userId=1 - 应返回转化统计")
  426. void getInviteConversionStats_shouldReturnStats() throws Exception {
  427. mockMvc.perform(get("/api/admin/invite/conversion-stats?userId=1")
  428. .header("Authorization", "Bearer " + adminToken()))
  429. .andExpect(status().isOk())
  430. .andExpect(jsonPath("$.code").value(200))
  431. .andExpect(jsonPath("$.data.inviteCode").exists())
  432. .andExpect(jsonPath("$.data.clickedCount").isNumber())
  433. .andExpect(jsonPath("$.data.totalInvited").isNumber())
  434. .andExpect(jsonPath("$.data.registeredCount").isNumber())
  435. .andExpect(jsonPath("$.data.pendingCount").isNumber())
  436. .andExpect(jsonPath("$.data.conversionRate").isNumber())
  437. .andExpect(jsonPath("$.data.totalReward").isNumber());
  438. }
  439. @Test
  440. @Order(41)
  441. @DisplayName("GET /api/admin/invite/conversion-stats - 转化率应在0-100之间")
  442. void getInviteConversionStats_conversionRateShouldBeInRange() throws Exception {
  443. mockMvc.perform(get("/api/admin/invite/conversion-stats?userId=1")
  444. .header("Authorization", "Bearer " + adminToken()))
  445. .andExpect(status().isOk())
  446. .andExpect(jsonPath("$.data.conversionRate").value(org.hamcrest.Matchers.greaterThanOrEqualTo(0.0)))
  447. .andExpect(jsonPath("$.data.conversionRate").value(org.hamcrest.Matchers.lessThanOrEqualTo(100.0)));
  448. }
  449. @Test
  450. @Order(42)
  451. @DisplayName("GET /api/admin/invite/conversion-stats - 待注册人数应>=0")
  452. void getInviteConversionStats_pendingCountShouldBeNonNegative() throws Exception {
  453. mockMvc.perform(get("/api/admin/invite/conversion-stats?userId=1")
  454. .header("Authorization", "Bearer " + adminToken()))
  455. .andExpect(status().isOk())
  456. .andExpect(jsonPath("$.data.pendingCount").value(org.hamcrest.Matchers.greaterThanOrEqualTo(0)));
  457. }
  458. @Test
  459. @Order(43)
  460. @DisplayName("GET /api/admin/invite/conversion-stats - 无Token应返回401")
  461. void getInviteConversionStats_withoutToken_shouldReturn401() throws Exception {
  462. mockMvc.perform(get("/api/admin/invite/conversion-stats?userId=1"))
  463. .andExpect(status().isUnauthorized());
  464. }
  465. }