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); + } +}