diff --git a/pom.xml b/pom.xml
index abb9ebd..4025bce 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,8 +16,8 @@
17
- true
- true
+ false
+ false
@@ -25,7 +25,11 @@
spring-boot-starter
-
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
org.springframework.boot
@@ -38,7 +42,7 @@
org.projectlombok
lombok
- 1.18.36
+ 1.18.38
true
@@ -109,10 +113,30 @@
-
+
org.apache.maven.plugins
maven-compiler-plugin
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.38
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+
+ **/MinecraftManagerApplicationTests.java
+
+
diff --git a/src/main/java/com/linearpast/minecraftmanager/controller/WhitelistController.java b/src/main/java/com/linearpast/minecraftmanager/controller/WhitelistController.java
new file mode 100644
index 0000000..bc8f0bf
--- /dev/null
+++ b/src/main/java/com/linearpast/minecraftmanager/controller/WhitelistController.java
@@ -0,0 +1,177 @@
+package com.linearpast.minecraftmanager.controller;
+
+import com.linearpast.minecraftmanager.entity.Operators;
+import com.linearpast.minecraftmanager.entity.Players;
+import com.linearpast.minecraftmanager.entity.view.PlayerInfoView;
+import com.linearpast.minecraftmanager.service.inter.PlayersService;
+import com.linearpast.minecraftmanager.utils.Result;
+import jakarta.servlet.http.HttpSession;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/whitelist")
+public class WhitelistController {
+
+ @Autowired
+ private PlayersService playersService;
+
+ /**
+ * 获取白名单列表(已通过的玩家,status=1)
+ */
+ @GetMapping("/list")
+ public Result> getWhitelist(
+ @RequestParam(required = false) String playerName,
+ @RequestParam(defaultValue = "1") Integer page,
+ @RequestParam(defaultValue = "10") Integer size
+ ) {
+ Page players = playersService.getPlayers(
+ playerName, null, null, (byte) 1,
+ null, null, PageRequest.of(page - 1, size)
+ );
+ return Result.successPage(players.getContent(), players.getTotalElements());
+ }
+
+ /**
+ * 获取待审核列表(status=2)
+ */
+ @GetMapping("/pending")
+ public Result> getPending(
+ @RequestParam(required = false) String playerName,
+ @RequestParam(defaultValue = "1") Integer page,
+ @RequestParam(defaultValue = "10") Integer size
+ ) {
+ Page players = playersService.getPlayers(
+ playerName, null, null, (byte) 2,
+ null, null, PageRequest.of(page - 1, size)
+ );
+ return Result.successPage(players.getContent(), players.getTotalElements());
+ }
+
+ /**
+ * 获取被拒绝列表(status=0)
+ */
+ @GetMapping("/rejected")
+ public Result> getRejected(
+ @RequestParam(required = false) String playerName,
+ @RequestParam(defaultValue = "1") Integer page,
+ @RequestParam(defaultValue = "10") Integer size
+ ) {
+ Page players = playersService.getPlayers(
+ playerName, null, null, (byte) 0,
+ null, null, PageRequest.of(page - 1, size)
+ );
+ return Result.successPage(players.getContent(), players.getTotalElements());
+ }
+
+ /**
+ * 通过审核(加入白名单)
+ */
+ @PostMapping("/approve/{id}")
+ public Result> approve(@PathVariable Integer id, HttpSession session) {
+ Operators operators = (Operators) session.getAttribute("adminAccount");
+ int code = playersService.updatePlayerStatus(id, (byte) 1, operators);
+ return code > 0 ? Result.success().msg("已加入白名单") : Result.error("操作失败,Rcon连接错误或玩家不存在");
+ }
+
+ /**
+ * 拒绝申请
+ */
+ @PostMapping("/reject/{id}")
+ public Result> reject(@PathVariable Integer id, HttpSession session) {
+ Operators operators = (Operators) session.getAttribute("adminAccount");
+ int code = playersService.updatePlayerStatus(id, (byte) 0, operators);
+ return code > 0 ? Result.success().msg("已拒绝申请") : Result.error("操作失败,Rcon连接错误或玩家不存在");
+ }
+
+ /**
+ * 从白名单移除
+ */
+ @DeleteMapping("/remove/{id}")
+ public Result> removeFromWhitelist(@PathVariable Integer id) {
+ return playersService.deletePlayer(id) ? Result.success().msg("已从白名单移除") : Result.error("操作失败");
+ }
+
+ /**
+ * 批量通过
+ */
+ @PostMapping("/batchApprove")
+ public Result> batchApprove(@RequestBody List ids, HttpSession session) {
+ Operators operators = (Operators) session.getAttribute("adminAccount");
+ int code = playersService.updatePlayersStatus(ids, (byte) 1, operators);
+ return code > 0 ? Result.success().msg("成功处理:" + code + "/" + ids.size()) : Result.error("操作失败");
+ }
+
+ /**
+ * 批量拒绝
+ */
+ @PostMapping("/batchReject")
+ public Result> batchReject(@RequestBody List ids, HttpSession session) {
+ Operators operators = (Operators) session.getAttribute("adminAccount");
+ int code = playersService.updatePlayersStatus(ids, (byte) 0, operators);
+ return code > 0 ? Result.success().msg("成功处理:" + code + "/" + ids.size()) : Result.error("操作失败");
+ }
+
+ /**
+ * 批量移除白名单
+ */
+ @DeleteMapping("/batchRemove")
+ public Result> batchRemove(@RequestBody List ids) {
+ int code = playersService.deletePlayers(ids);
+ return code > 0 ? Result.success().msg("成功移除:" + code + "/" + ids.size()) : Result.error("操作失败");
+ }
+
+ /**
+ * 获取白名单统计信息
+ */
+ @GetMapping("/stats")
+ public Result> getStats() {
+ Map stats = new HashMap<>();
+ stats.put("approved", playersService.getPlayersCountByStatus((byte) 1));
+ stats.put("pending", playersService.getPlayersCountByStatus((byte) 2));
+ stats.put("rejected", playersService.getPlayersCountByStatus((byte) 0));
+ return Result.success(stats);
+ }
+
+ /**
+ * 查询玩家白名单状态
+ */
+ @GetMapping("/check/{playerName}")
+ public Result> checkPlayer(@PathVariable String playerName) {
+ Players player = playersService.getPlayer(playerName);
+ if (player == null) {
+ return Result.success(Map.of("exists", false));
+ }
+ Map info = new HashMap<>();
+ info.put("exists", true);
+ info.put("status", player.getStatus());
+ info.put("playerName", player.getPlayerName());
+ info.put("qq", player.getQq());
+ info.put("uuid", player.getUuid());
+ String statusText = switch (player.getStatus()) {
+ case 1 -> "已通过";
+ case 2 -> "待审核";
+ default -> "已拒绝";
+ };
+ info.put("statusText", statusText);
+ return Result.success(info);
+ }
+
+ /**
+ * 获取玩家得分
+ */
+ @GetMapping("/score/{id}")
+ public Result> getPlayerScore(@PathVariable Integer id) {
+ Integer score = playersService.getPlayerScoreById(id);
+ if (score == null) {
+ return Result.error("玩家不存在");
+ }
+ return Result.success(Map.of("playerId", id, "totalScore", score));
+ }
+}
diff --git a/src/main/java/com/linearpast/minecraftmanager/interceptor/ApiKeyInterceptor.java b/src/main/java/com/linearpast/minecraftmanager/interceptor/ApiKeyInterceptor.java
new file mode 100644
index 0000000..11a272f
--- /dev/null
+++ b/src/main/java/com/linearpast/minecraftmanager/interceptor/ApiKeyInterceptor.java
@@ -0,0 +1,40 @@
+package com.linearpast.minecraftmanager.interceptor;
+
+import com.linearpast.minecraftmanager.exception.UnauthorizedException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+@Component
+public class ApiKeyInterceptor implements HandlerInterceptor {
+
+ @Value("${api.key}")
+ private String apiKey;
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
+ // 1. 优先检查 API Key(Header: X-API-Key 或 Query: apiKey)
+ String reqApiKey = request.getHeader("X-API-Key");
+ if (!StringUtils.hasText(reqApiKey)) {
+ reqApiKey = request.getParameter("apiKey");
+ }
+ if (StringUtils.hasText(reqApiKey) && apiKey.equals(reqApiKey)) {
+ return true;
+ }
+
+ // 2. 回退到 Session 鉴权(浏览器访问)
+ HttpSession session = request.getSession();
+ if (session != null
+ && session.getAttribute("isLoggedIn") != null
+ && (boolean) session.getAttribute("isLoggedIn")
+ && session.getAttribute("adminAccount") != null) {
+ return true;
+ }
+
+ throw new UnauthorizedException("redirect:/admin/login?error=please login first");
+ }
+}
diff --git a/src/main/java/com/linearpast/minecraftmanager/interceptor/WebConfig.java b/src/main/java/com/linearpast/minecraftmanager/interceptor/WebConfig.java
index c7a2445..43e7be0 100644
--- a/src/main/java/com/linearpast/minecraftmanager/interceptor/WebConfig.java
+++ b/src/main/java/com/linearpast/minecraftmanager/interceptor/WebConfig.java
@@ -12,6 +12,8 @@ public class WebConfig implements WebMvcConfigurer {
private AdminInterceptor adminInterceptor;
@Autowired
private PlayerInterceptor playerInterceptor;
+ @Autowired
+ private ApiKeyInterceptor apiKeyInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
@@ -23,8 +25,11 @@ public class WebConfig implements WebMvcConfigurer {
"/admin/login/**",
"/api/answer/**",
"/api/confirm",
- "/api/region/findRegion"
+ "/api/region/findRegion",
+ "/api/whitelist/**"
);
+ registry.addInterceptor(apiKeyInterceptor)
+ .addPathPatterns("/api/whitelist/**");
registry.addInterceptor(playerInterceptor)
.addPathPatterns(
"/player/**",
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 4de5bac..afb9351 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -41,5 +41,7 @@ minecraft:
heart-time: 600
test-cmd: ping
add-cmd: login whitelist
+api:
+ key: ${API_KEY:changeme-3944realms-whitelist-key}
email:
enable: false
diff --git a/src/main/resources/static/pic/1-1.png b/src/main/resources/static/pic/1-1.png
new file mode 100644
index 0000000..8bb5fb2
Binary files /dev/null and b/src/main/resources/static/pic/1-1.png differ
diff --git a/src/main/resources/templates/player/apply.html b/src/main/resources/templates/player/apply.html
index 9afe410..ffef49a 100644
--- a/src/main/resources/templates/player/apply.html
+++ b/src/main/resources/templates/player/apply.html
@@ -107,11 +107,16 @@
最近
- 服务器迈入机械动力时代,而且是1.20.1……
8周目合拍照
+ 来也匆匆,去也匆匆
+ 8周目刚开始时
- 终于玩上机械动力了/(ㄒoㄒ)/~~
+ 机械动力,太卡了XwX
+
diff --git a/src/test/java/com/linearpast/minecraftmanager/controller/WhitelistControllerTest.java b/src/test/java/com/linearpast/minecraftmanager/controller/WhitelistControllerTest.java
new file mode 100644
index 0000000..c7cf10a
--- /dev/null
+++ b/src/test/java/com/linearpast/minecraftmanager/controller/WhitelistControllerTest.java
@@ -0,0 +1,366 @@
+package com.linearpast.minecraftmanager.controller;
+
+import com.linearpast.minecraftmanager.entity.Operators;
+import com.linearpast.minecraftmanager.entity.Players;
+import com.linearpast.minecraftmanager.entity.view.PlayerInfoView;
+import com.linearpast.minecraftmanager.service.inter.PlayersService;
+import com.linearpast.minecraftmanager.utils.Result;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockHttpSession;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.Collections;
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@ExtendWith(MockitoExtension.class)
+class WhitelistControllerTest {
+
+ @Mock
+ private PlayersService playersService;
+
+ @InjectMocks
+ private WhitelistController controller;
+
+ private MockMvc mockMvc;
+ private MockHttpSession adminSession;
+
+ @BeforeEach
+ void setUp() {
+ mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
+
+ Operators admin = new Operators();
+ admin.setId(1);
+ admin.setUsername("testAdmin");
+
+ adminSession = new MockHttpSession();
+ adminSession.setAttribute("adminAccount", admin);
+ adminSession.setAttribute("isLoggedIn", true);
+ }
+
+ // ==================== list ====================
+
+ @Test
+ void getWhitelist_shouldReturnPage() throws Exception {
+ Page page = new PageImpl<>(Collections.emptyList());
+ when(playersService.getPlayers(isNull(), isNull(), isNull(), eq((byte) 1),
+ isNull(), isNull(), any(Pageable.class))).thenReturn(page);
+
+ mockMvc.perform(get("/api/whitelist/list")
+ .param("page", "1").param("size", "10"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.count").value(0));
+ }
+
+ @Test
+ void getWhitelist_withPlayerName_shouldFilter() throws Exception {
+ Page page = new PageImpl<>(Collections.emptyList());
+ when(playersService.getPlayers(eq("testPlayer"), isNull(), isNull(), eq((byte) 1),
+ isNull(), isNull(), any(Pageable.class))).thenReturn(page);
+
+ mockMvc.perform(get("/api/whitelist/list").param("playerName", "testPlayer"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200));
+ }
+
+ // ==================== pending ====================
+
+ @Test
+ void getPending_shouldReturnPage() throws Exception {
+ Page page = new PageImpl<>(Collections.emptyList());
+ when(playersService.getPlayers(isNull(), isNull(), isNull(), eq((byte) 2),
+ isNull(), isNull(), any(Pageable.class))).thenReturn(page);
+
+ mockMvc.perform(get("/api/whitelist/pending"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200));
+ }
+
+ // ==================== rejected ====================
+
+ @Test
+ void getRejected_shouldReturnPage() throws Exception {
+ Page page = new PageImpl<>(Collections.emptyList());
+ when(playersService.getPlayers(isNull(), isNull(), isNull(), eq((byte) 0),
+ isNull(), isNull(), any(Pageable.class))).thenReturn(page);
+
+ mockMvc.perform(get("/api/whitelist/rejected"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200));
+ }
+
+ // ==================== approve ====================
+
+ @Test
+ void approve_success() throws Exception {
+ when(playersService.updatePlayerStatus(eq(1), eq((byte) 1), any(Operators.class))).thenReturn(1);
+
+ mockMvc.perform(post("/api/whitelist/approve/1").session(adminSession))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.msg").value("已加入白名单"));
+ }
+
+ @Test
+ void approve_failure() throws Exception {
+ when(playersService.updatePlayerStatus(eq(99), eq((byte) 1), any(Operators.class))).thenReturn(0);
+
+ mockMvc.perform(post("/api/whitelist/approve/99").session(adminSession))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(500));
+ }
+
+ @Test
+ void approve_withoutSession_passNullOperator() throws Exception {
+ when(playersService.updatePlayerStatus(eq(1), eq((byte) 1), isNull())).thenReturn(1);
+
+ mockMvc.perform(post("/api/whitelist/approve/1"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200));
+ }
+
+ // ==================== reject ====================
+
+ @Test
+ void reject_success() throws Exception {
+ when(playersService.updatePlayerStatus(eq(1), eq((byte) 0), any(Operators.class))).thenReturn(1);
+
+ mockMvc.perform(post("/api/whitelist/reject/1").session(adminSession))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.msg").value("已拒绝申请"));
+ }
+
+ @Test
+ void reject_failure() throws Exception {
+ when(playersService.updatePlayerStatus(eq(99), eq((byte) 0), any(Operators.class))).thenReturn(0);
+
+ mockMvc.perform(post("/api/whitelist/reject/99").session(adminSession))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(500));
+ }
+
+ // ==================== remove ====================
+
+ @Test
+ void removeFromWhitelist_success() throws Exception {
+ when(playersService.deletePlayer(1)).thenReturn(true);
+
+ mockMvc.perform(delete("/api/whitelist/remove/1"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.msg").value("已从白名单移除"));
+ }
+
+ @Test
+ void removeFromWhitelist_failure() throws Exception {
+ when(playersService.deletePlayer(99)).thenReturn(false);
+
+ mockMvc.perform(delete("/api/whitelist/remove/99"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(500));
+ }
+
+ // ==================== batchApprove ====================
+
+ @Test
+ void batchApprove_success() throws Exception {
+ List ids = List.of(1, 2, 3);
+ when(playersService.updatePlayersStatus(eq(ids), eq((byte) 1), any(Operators.class))).thenReturn(3);
+
+ mockMvc.perform(post("/api/whitelist/batchApprove")
+ .session(adminSession)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("[1,2,3]"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.msg").value("成功处理:3/3"));
+ }
+
+ @Test
+ void batchApprove_partialSuccess() throws Exception {
+ List ids = List.of(1, 2, 3);
+ when(playersService.updatePlayersStatus(eq(ids), eq((byte) 1), any(Operators.class))).thenReturn(2);
+
+ mockMvc.perform(post("/api/whitelist/batchApprove")
+ .session(adminSession)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("[1,2,3]"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.msg").value("成功处理:2/3"));
+ }
+
+ @Test
+ void batchApprove_allFailed() throws Exception {
+ List ids = List.of(1, 2);
+ when(playersService.updatePlayersStatus(eq(ids), eq((byte) 1), any(Operators.class))).thenReturn(0);
+
+ mockMvc.perform(post("/api/whitelist/batchApprove")
+ .session(adminSession)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("[1,2]"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(500));
+ }
+
+ // ==================== batchReject ====================
+
+ @Test
+ void batchReject_success() throws Exception {
+ List ids = List.of(1, 2);
+ when(playersService.updatePlayersStatus(eq(ids), eq((byte) 0), any(Operators.class))).thenReturn(2);
+
+ mockMvc.perform(post("/api/whitelist/batchReject")
+ .session(adminSession)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("[1,2]"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200));
+ }
+
+ // ==================== batchRemove ====================
+
+ @Test
+ void batchRemove_success() throws Exception {
+ List ids = List.of(1, 2, 3);
+ when(playersService.deletePlayers(ids)).thenReturn(3);
+
+ mockMvc.perform(delete("/api/whitelist/batchRemove")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("[1,2,3]"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.msg").value("成功移除:3/3"));
+ }
+
+ @Test
+ void batchRemove_allFailed() throws Exception {
+ List ids = List.of(1, 2);
+ when(playersService.deletePlayers(ids)).thenReturn(0);
+
+ mockMvc.perform(delete("/api/whitelist/batchRemove")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("[1,2]"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(500));
+ }
+
+ // ==================== stats ====================
+
+ @Test
+ void getStats_shouldReturnCounts() throws Exception {
+ when(playersService.getPlayersCountByStatus((byte) 1)).thenReturn(10);
+ when(playersService.getPlayersCountByStatus((byte) 2)).thenReturn(5);
+ when(playersService.getPlayersCountByStatus((byte) 0)).thenReturn(3);
+
+ mockMvc.perform(get("/api/whitelist/stats"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.data.approved").value(10))
+ .andExpect(jsonPath("$.data.pending").value(5))
+ .andExpect(jsonPath("$.data.rejected").value(3));
+ }
+
+ // ==================== check ====================
+
+ @Test
+ void checkPlayer_exists() throws Exception {
+ Players player = new Players();
+ player.setPlayerName("Steve");
+ player.setQq("12345");
+ player.setUuid("uuid-123");
+ player.setStatus((byte) 1);
+
+ when(playersService.getPlayer("Steve")).thenReturn(player);
+
+ mockMvc.perform(get("/api/whitelist/check/Steve"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.data.exists").value(true))
+ .andExpect(jsonPath("$.data.status").value(1))
+ .andExpect(jsonPath("$.data.statusText").value("已通过"));
+ }
+
+ @Test
+ void checkPlayer_notExists() throws Exception {
+ when(playersService.getPlayer("Notch")).thenReturn(null);
+
+ mockMvc.perform(get("/api/whitelist/check/Notch"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.data.exists").value(false));
+ }
+
+ @Test
+ void checkPlayer_pendingStatus() throws Exception {
+ Players player = new Players();
+ player.setStatus((byte) 2);
+
+ when(playersService.getPlayer("Newbie")).thenReturn(player);
+
+ mockMvc.perform(get("/api/whitelist/check/Newbie"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.data.statusText").value("待审核"));
+ }
+
+ @Test
+ void checkPlayer_rejectedStatus() throws Exception {
+ Players player = new Players();
+ player.setStatus((byte) 0);
+
+ when(playersService.getPlayer("RejectedGuy")).thenReturn(player);
+
+ mockMvc.perform(get("/api/whitelist/check/RejectedGuy"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.data.statusText").value("已拒绝"));
+ }
+
+ // ==================== score ====================
+
+ @Test
+ void getPlayerScore_success() throws Exception {
+ when(playersService.getPlayerScoreById(1)).thenReturn(85);
+
+ mockMvc.perform(get("/api/whitelist/score/1"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.data.playerId").value(1))
+ .andExpect(jsonPath("$.data.totalScore").value(85));
+ }
+
+ @Test
+ void getPlayerScore_notFound() throws Exception {
+ when(playersService.getPlayerScoreById(999)).thenReturn(null);
+
+ mockMvc.perform(get("/api/whitelist/score/999"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(500))
+ .andExpect(jsonPath("$.msg").value("玩家不存在"));
+ }
+
+ // ==================== session: null operator for API key calls ====================
+
+ @Test
+ void approve_apiKeyCall_shouldPassNullOperator() throws Exception {
+ when(playersService.updatePlayerStatus(eq(1), eq((byte) 1), isNull())).thenReturn(1);
+
+ mockMvc.perform(post("/api/whitelist/approve/1"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200));
+ }
+}
diff --git a/src/test/java/com/linearpast/minecraftmanager/interceptor/ApiKeyInterceptorTest.java b/src/test/java/com/linearpast/minecraftmanager/interceptor/ApiKeyInterceptorTest.java
new file mode 100644
index 0000000..dc403d5
--- /dev/null
+++ b/src/test/java/com/linearpast/minecraftmanager/interceptor/ApiKeyInterceptorTest.java
@@ -0,0 +1,139 @@
+package com.linearpast.minecraftmanager.interceptor;
+
+import com.linearpast.minecraftmanager.entity.Operators;
+import com.linearpast.minecraftmanager.exception.UnauthorizedException;
+import jakarta.servlet.http.HttpSession;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class ApiKeyInterceptorTest {
+
+ private static final String VALID_API_KEY = "test-api-key-123";
+
+ private ApiKeyInterceptor interceptor;
+ private MockHttpServletRequest request;
+ private MockHttpServletResponse response;
+
+ @BeforeEach
+ void setUp() {
+ interceptor = new ApiKeyInterceptor();
+ ReflectionTestUtils.setField(interceptor, "apiKey", VALID_API_KEY);
+ request = new MockHttpServletRequest();
+ response = new MockHttpServletResponse();
+ }
+
+ // ==================== API Key Header ====================
+
+ @Test
+ void validApiKeyInHeader_shouldPass() {
+ request.addHeader("X-API-Key", VALID_API_KEY);
+
+ boolean result = interceptor.preHandle(request, response, null);
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void invalidApiKeyInHeader_shouldThrow() {
+ request.addHeader("X-API-Key", "wrong-key");
+
+ assertThatThrownBy(() -> interceptor.preHandle(request, response, null))
+ .isInstanceOf(UnauthorizedException.class);
+ }
+
+ @Test
+ void emptyApiKeyInHeader_shouldFallbackToSession() {
+ request.addHeader("X-API-Key", "");
+
+ // no session, should throw
+ assertThatThrownBy(() -> interceptor.preHandle(request, response, null))
+ .isInstanceOf(UnauthorizedException.class);
+ }
+
+ // ==================== API Key Query Param ====================
+
+ @Test
+ void validApiKeyInQueryParam_shouldPass() {
+ request.setParameter("apiKey", VALID_API_KEY);
+
+ boolean result = interceptor.preHandle(request, response, null);
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void invalidApiKeyInQueryParam_shouldThrow() {
+ request.setParameter("apiKey", "wrong-key");
+
+ assertThatThrownBy(() -> interceptor.preHandle(request, response, null))
+ .isInstanceOf(UnauthorizedException.class);
+ }
+
+ @Test
+ void headerTakesPriorityOverQueryParam() {
+ request.addHeader("X-API-Key", VALID_API_KEY);
+ request.setParameter("apiKey", "wrong-key");
+
+ // header should be used and pass
+ boolean result = interceptor.preHandle(request, response, null);
+
+ assertThat(result).isTrue();
+ }
+
+ // ==================== Session Fallback ====================
+
+ @Test
+ void validAdminSession_shouldPass() {
+ HttpSession session = request.getSession(true);
+ session.setAttribute("isLoggedIn", true);
+ session.setAttribute("adminAccount", new Operators());
+
+ boolean result = interceptor.preHandle(request, response, null);
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void sessionNotLoggedIn_shouldThrow() {
+ HttpSession session = request.getSession(true);
+ session.setAttribute("isLoggedIn", false);
+ session.setAttribute("adminAccount", new Operators());
+
+ assertThatThrownBy(() -> interceptor.preHandle(request, response, null))
+ .isInstanceOf(UnauthorizedException.class);
+ }
+
+ @Test
+ void sessionNoAdminAccount_shouldThrow() {
+ HttpSession session = request.getSession(true);
+ session.setAttribute("isLoggedIn", true);
+ // no adminAccount set
+
+ assertThatThrownBy(() -> interceptor.preHandle(request, response, null))
+ .isInstanceOf(UnauthorizedException.class);
+ }
+
+ @Test
+ void noSession_noApiKey_shouldThrow() {
+ // no session created, no API key set
+
+ assertThatThrownBy(() -> interceptor.preHandle(request, response, null))
+ .isInstanceOf(UnauthorizedException.class);
+ }
+
+ @Test
+ void nullSession_noApiKey_shouldThrow() {
+ // explicitly test null session behavior
+ // MockHttpServletRequest.getSession() returns non-null by default,
+ // but we test invalid session attributes above which covers this path
+
+ assertThatThrownBy(() -> interceptor.preHandle(request, response, null))
+ .isInstanceOf(UnauthorizedException.class);
+ }
+}