feat: 添加API接口,编写单元测试

This commit is contained in:
叁玖领域 2026-06-09 11:45:42 +08:00
parent 377ca8cba3
commit a6ee2ac518
9 changed files with 766 additions and 8 deletions

34
pom.xml
View File

@ -16,8 +16,8 @@
<properties> <properties>
<java.version>17</java.version> <java.version>17</java.version>
<!-- 跳过测试 --> <!-- 跳过测试 -->
<skipTests>true</skipTests> <skipTests>false</skipTests>
<maven.test.skip>true</maven.test.skip> <maven.test.skip>false</maven.test.skip>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
@ -25,7 +25,11 @@
<artifactId>spring-boot-starter</artifactId> <artifactId>spring-boot-starter</artifactId>
</dependency> </dependency>
<!-- 移除了spring-boot-starter-test依赖 --> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@ -38,7 +42,7 @@
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<version>1.18.36</version> <version>1.18.38</version>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
@ -109,10 +113,30 @@
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
<!-- 配置maven-compiler-plugin跳过测试 --> <!-- 配置maven-compiler-plugin -->
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- 配置maven-surefire-plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- 排除需要数据库的 @SpringBootTest -->
<excludes>
<exclude>**/MinecraftManagerApplicationTests.java</exclude>
</excludes>
</configuration>
</plugin> </plugin>
</plugins> </plugins>
<resources> <resources>

View File

@ -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<PlayerInfoView> 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<PlayerInfoView> 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<PlayerInfoView> 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<Integer> 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<Integer> 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<Integer> ids) {
int code = playersService.deletePlayers(ids);
return code > 0 ? Result.success().msg("成功移除:" + code + "/" + ids.size()) : Result.error("操作失败");
}
/**
* 获取白名单统计信息
*/
@GetMapping("/stats")
public Result<?> getStats() {
Map<String, Object> 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<String, Object> 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));
}
}

View File

@ -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 KeyHeader: 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");
}
}

View File

@ -12,6 +12,8 @@ public class WebConfig implements WebMvcConfigurer {
private AdminInterceptor adminInterceptor; private AdminInterceptor adminInterceptor;
@Autowired @Autowired
private PlayerInterceptor playerInterceptor; private PlayerInterceptor playerInterceptor;
@Autowired
private ApiKeyInterceptor apiKeyInterceptor;
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
@ -23,8 +25,11 @@ public class WebConfig implements WebMvcConfigurer {
"/admin/login/**", "/admin/login/**",
"/api/answer/**", "/api/answer/**",
"/api/confirm", "/api/confirm",
"/api/region/findRegion" "/api/region/findRegion",
"/api/whitelist/**"
); );
registry.addInterceptor(apiKeyInterceptor)
.addPathPatterns("/api/whitelist/**");
registry.addInterceptor(playerInterceptor) registry.addInterceptor(playerInterceptor)
.addPathPatterns( .addPathPatterns(
"/player/**", "/player/**",

View File

@ -41,5 +41,7 @@ minecraft:
heart-time: 600 heart-time: 600
test-cmd: ping test-cmd: ping
add-cmd: login whitelist add-cmd: login whitelist
api:
key: ${API_KEY:changeme-3944realms-whitelist-key}
email: email:
enable: false enable: false

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 MiB

View File

@ -107,11 +107,16 @@
<div class="timeline-badge primary"></div> <div class="timeline-badge primary"></div>
<a class="timeline-panel text-muted" href="javascript:void(0);"> <a class="timeline-panel text-muted" href="javascript:void(0);">
<span>最近</span> <span>最近</span>
<h6 class="mb-0">服务器迈入机械动力时代而且是1.20.1……<img alt="" <h6 class="mb-0">8周目合拍照<img alt=""
class="img-fluid w-100"
src="/pic/1-1.png"></h6>
<p class="mb-0">来也匆匆,去也匆匆</p>
<h6 class="mb-0">8周目刚开始时<img alt=""
class="img-fluid w-100" class="img-fluid w-100"
src="/pic/1_1.png"></h6> src="/pic/1_1.png"></h6>
<p class="mb-0">终于玩上机械动力了/(ㄒoㄒ)/~~</p> <p class="mb-0">机械动力太卡了XwX</p>
</a> </a>
</li> </li>
<li> <li>
<div class="timeline-badge info"></div> <div class="timeline-badge info"></div>

View File

@ -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<PlayerInfoView> 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<PlayerInfoView> 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<PlayerInfoView> 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<PlayerInfoView> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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));
}
}

View File

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