Compare commits

..

13 Commits

Author SHA1 Message Date
a6ee2ac518 feat: 添加API接口,编写单元测试 2026-06-09 11:45:42 +08:00
377ca8cba3 修正 2025-10-31 23:19:37 +08:00
80111e7141 修正 2025-10-31 23:15:37 +08:00
3944Realms
61dded61ed
Action 2025-10-31 23:03:10 +08:00
3944Realms
e382d5f6f7
Merge branch 'Linearpast:master' into master 2025-10-31 23:02:03 +08:00
LostInLinearPast
117f7d222c update blank question rule
version 1.0.2
2025-10-31 22:57:38 +08:00
LostInLinearPast
a64da463ff update blank question rule
version 1.0.2
2025-10-31 22:51:28 +08:00
a3622b0e72 修正 2025-10-31 17:06:39 +08:00
820a3615be 修正 2025-10-31 17:05:08 +08:00
3944Realms
7d6bd52e37
Merge branch 'Linearpast:master' into master 2025-10-31 16:59:02 +08:00
LostInLinearPast
9d8fa35aad update blank question rule
version 1.0.2
2025-10-31 14:14:05 +08:00
LostInLinearPast
4054862c70 update blank question rule
version 1.0.2
2025-10-31 13:59:13 +08:00
LostInLinearPast
024a9672d5 update blank question rule 2025-10-31 13:58:45 +08:00
18 changed files with 895 additions and 137 deletions

31
.github/workflows/maven-publish.yml vendored Normal file
View File

@ -0,0 +1,31 @@
name: Maven Package
on:
release:
types: [created]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
server-id: github
settings-path: ${{ github.workspace }}
- name: Build with Maven
run: mvn -B package --file pom.xml
- name: Publish to GitHub Packages Apache Maven
run: mvn deploy -s $GITHUB_WORKSPACE/settings.xml -DaltDeploymentRepository=github::default::https://maven.pkg.github.com/${{ github.repository }}
env:
GITHUB_TOKEN: ${{ github.token }}

1
.gitignore vendored
View File

@ -33,3 +33,4 @@ build/
.vscode/ .vscode/
out/ out/
src/main/resources/bak.yml

44
pom.xml
View File

@ -10,14 +10,14 @@
</parent> </parent>
<groupId>com.linearpast</groupId> <groupId>com.linearpast</groupId>
<artifactId>MinecraftManager</artifactId> <artifactId>MinecraftManager</artifactId>
<version>1.0.1</version> <version>1.0.2</version>
<name>MinecraftManager</name> <name>MinecraftManager</name>
<description>MinecraftManager</description> <description>MinecraftManager</description>
<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>
@ -124,5 +148,11 @@
</resource> </resource>
</resources> </resources>
</build> </build>
<distributionManagement>
<repository>
<id>github</id>
<name>GitHub Packages</name>
<url>https://maven.pkg.github.com/LeisureTimeDock/MinecraftLTDWhitelistSystem</url>
</repository>
</distributionManagement>
</project> </project>

View File

@ -202,7 +202,7 @@ public class PlayerController {
question.setOptions(result.toString()); question.setOptions(result.toString());
} else if(type == 2){ } else if(type == 2){
String options = question.getOptions(); String options = question.getOptions();
String processed = options.replaceAll("\\$\\{.*?}", "\\${}"); String processed = options.replaceAll("\\$(\\d+)?\\{.*?}", "\\${}");
String[] parts = processed.split("(?<=\\$\\{})|(?=\\$\\{})"); String[] parts = processed.split("(?<=\\$\\{})|(?=\\$\\{})");
JsonArray result = new Gson().toJsonTree(Arrays.asList(parts)).getAsJsonArray(); JsonArray result = new Gson().toJsonTree(Arrays.asList(parts)).getAsJsonArray();
question.setOptions(result.toString()); question.setOptions(result.toString());
@ -280,20 +280,46 @@ public class PlayerController {
List<String> resultList = new ArrayList<>(); List<String> resultList = new ArrayList<>();
JsonArray asJsonArray = JsonParser.parseString(answer).getAsJsonArray(); JsonArray asJsonArray = JsonParser.parseString(answer).getAsJsonArray();
for (JsonElement element : asJsonArray) { for (JsonElement element : asJsonArray) {
resultList.add(element.getAsString()); resultList.add(element.getAsString().trim());
} }
String regex = "\\$\\{(.+?)}"; String regex = "\\$(\\d+)?\\{(.+?)}";
Pattern pattern = Pattern.compile(regex); Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(questions.getOptions()); Matcher matcher = pattern.matcher(questions.getOptions());
List<String> result = new ArrayList<>(); int score = 0;
int i = 0;
int j = 0; //没有标记分数的答对题目数
int k = 0; //没有标记分数的答案数量
//后续计算之后表示所有未标记分数的答案的总分
int maxScore = questions.getScore();
while (matcher.find()) { while (matcher.find()) {
result.add(matcher.group(1)); String scoreString = matcher.group(1);
String answerString = matcher.group(2);
int parseInt = 0;
try {parseInt = Integer.parseInt(scoreString);}
catch (NumberFormatException ignored) {}
//若没有标记分数k++
//若标记了分数最大分数减去后续计算未标记分数的答案的均分
if(parseInt == 0) k++;
else maxScore -= parseInt;
//或逻辑
if (Arrays.stream(answerString.split("\\|")).toList().contains(resultList.get(i))) {
score += parseInt;
if(parseInt == 0) j++;
}
i++;
} }
if(resultList.equals(result)){ if(k > 0) {
playerAnswers.setScore(questions.getScore()); //未标记分数的答案的每题平均分
}else { float averageScore = (float)maxScore / k;
playerAnswers.setScore(0); //答对的未标记 * 未标记的平均分 = 未标记的答对总分
int rightScore;
//如果答对的未标记 == 总未标记直接给未标记的满分为了弥补四舍五入带来的误差
//否则直接给 未标记平均分 * 未标记答对
if(j == k) rightScore = maxScore;
else rightScore = Math.round(averageScore * j);
score += rightScore;
} }
playerAnswers.setScore(score);
} }
answers.add(playerAnswers); answers.add(playerAnswers);
} }

View File

@ -14,6 +14,8 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@RestController @RestController
@RequestMapping("/api/questions") @RequestMapping("/api/questions")
@ -25,6 +27,10 @@ public class QuestionController {
public Result<?> saveQuestion(@RequestBody QuestionSaveDTO questionSaveDTO) { public Result<?> saveQuestion(@RequestBody QuestionSaveDTO questionSaveDTO) {
if (questionSaveDTO != null) { if (questionSaveDTO != null) {
Questions questions = new Questions(); Questions questions = new Questions();
if(questionSaveDTO.getId() != null) questions.setId(questionSaveDTO.getId());
questions.setTitle(questionSaveDTO.getTitle());
questions.setScore(questionSaveDTO.getScore());
questions.setType(questionSaveDTO.getType());
if(questionSaveDTO.getType() == (byte) 1){ if(questionSaveDTO.getType() == (byte) 1){
JsonArray inputArray = new JsonArray(); JsonArray inputArray = new JsonArray();
JsonArray correct = new JsonArray(); JsonArray correct = new JsonArray();
@ -45,11 +51,20 @@ public class QuestionController {
questions.setOptions(resultArray.toString()); questions.setOptions(resultArray.toString());
} else if (questionSaveDTO.getType() == (byte) 2) { } else if (questionSaveDTO.getType() == (byte) 2) {
questions.setOptions(questionSaveDTO.getBlankContent()); questions.setOptions(questionSaveDTO.getBlankContent());
Integer originScore = questions.getScore();
String regex = "\\$(\\d+)\\{(.+?)}";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(questions.getOptions());
int score = 0;
while (matcher.find()) {
String scoreString = matcher.group(1);
int parseInt = 0;
try {parseInt = Integer.parseInt(scoreString);}
catch (NumberFormatException ignored) {}
score += parseInt;
}
if(score > originScore) questions.setScore(score);
} }
if(questionSaveDTO.getId() != null) questions.setId(questionSaveDTO.getId());
questions.setTitle(questionSaveDTO.getTitle());
questions.setScore(questionSaveDTO.getScore());
questions.setType(questionSaveDTO.getType());
Questions result = questionsService.saveQuestions(questions); Questions result = questionsService.saveQuestions(questions);
return result == null ? Result.error("服务器错误") : Result.success(); return result == null ? Result.error("服务器错误") : Result.success();
} }

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

@ -8,6 +8,7 @@ import org.springframework.core.env.PropertySource;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects;
public class ConfigLoader implements EnvironmentPostProcessor { public class ConfigLoader implements EnvironmentPostProcessor {
public static final Map<String, String> config = new HashMap<>(); public static final Map<String, String> config = new HashMap<>();
@ -20,7 +21,7 @@ public class ConfigLoader implements EnvironmentPostProcessor {
).findFirst().orElseThrow(); ).findFirst().orElseThrow();
if(source instanceof MapPropertySource mapPropertySource) { if(source instanceof MapPropertySource mapPropertySource) {
for (String key : mapPropertySource.getPropertyNames()) { for (String key : mapPropertySource.getPropertyNames()) {
config.put(key, mapPropertySource.getProperty(key).toString()); config.put(key, Objects.requireNonNull(mapPropertySource.getProperty(key)).toString());
System.out.println(key + "=" + mapPropertySource.getProperty(key)); System.out.println(key + "=" + mapPropertySource.getProperty(key));
} }
} }

View File

@ -33,17 +33,6 @@ import java.util.Map;
public class HttpUtils { public class HttpUtils {
/**
* get
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @return
* @throws Exception
*/
public static HttpResponse doGet(String host, String path, String method, public static HttpResponse doGet(String host, String path, String method,
Map<String, String> headers, Map<String, String> headers,
Map<String, String> querys) Map<String, String> querys)
@ -58,18 +47,6 @@ public class HttpUtils {
return httpClient.execute(request); return httpClient.execute(request);
} }
/**
* post form
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param bodys
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method, public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers, Map<String, String> headers,
Map<String, String> querys, Map<String, String> querys,
@ -96,18 +73,6 @@ public class HttpUtils {
return httpClient.execute(request); return httpClient.execute(request);
} }
/**
* Post String
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method, public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers, Map<String, String> headers,
Map<String, String> querys, Map<String, String> querys,
@ -127,18 +92,6 @@ public class HttpUtils {
return httpClient.execute(request); return httpClient.execute(request);
} }
/**
* Post stream
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method, public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers, Map<String, String> headers,
Map<String, String> querys, Map<String, String> querys,
@ -158,17 +111,6 @@ public class HttpUtils {
return httpClient.execute(request); return httpClient.execute(request);
} }
/**
* Put String
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPut(String host, String path, String method, public static HttpResponse doPut(String host, String path, String method,
Map<String, String> headers, Map<String, String> headers,
Map<String, String> querys, Map<String, String> querys,
@ -188,17 +130,6 @@ public class HttpUtils {
return httpClient.execute(request); return httpClient.execute(request);
} }
/**
* Put stream
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPut(String host, String path, String method, public static HttpResponse doPut(String host, String path, String method,
Map<String, String> headers, Map<String, String> headers,
Map<String, String> querys, Map<String, String> querys,
@ -218,17 +149,6 @@ public class HttpUtils {
return httpClient.execute(request); return httpClient.execute(request);
} }
/**
* Delete
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @return
* @throws Exception
*/
public static HttpResponse doDelete(String host, String path, String method, public static HttpResponse doDelete(String host, String path, String method,
Map<String, String> headers, Map<String, String> headers,
Map<String, String> querys) Map<String, String> querys)
@ -252,7 +172,7 @@ public class HttpUtils {
if (null != querys) { if (null != querys) {
StringBuilder sbQuery = new StringBuilder(); StringBuilder sbQuery = new StringBuilder();
for (Map.Entry<String, String> query : querys.entrySet()) { for (Map.Entry<String, String> query : querys.entrySet()) {
if (0 < sbQuery.length()) { if (!sbQuery.isEmpty()) {
sbQuery.append("&"); sbQuery.append("&");
} }
if (StringUtils.isBlank(query.getKey()) && !StringUtils.isBlank(query.getValue())) { if (StringUtils.isBlank(query.getKey()) && !StringUtils.isBlank(query.getValue())) {
@ -266,7 +186,7 @@ public class HttpUtils {
} }
} }
} }
if (0 < sbQuery.length()) { if (!sbQuery.isEmpty()) {
sbUrl.append("?").append(sbQuery); sbUrl.append("?").append(sbQuery);
} }
} }
@ -303,9 +223,7 @@ public class HttpUtils {
ClientConnectionManager ccm = httpClient.getConnectionManager(); ClientConnectionManager ccm = httpClient.getConnectionManager();
SchemeRegistry registry = ccm.getSchemeRegistry(); SchemeRegistry registry = ccm.getSchemeRegistry();
registry.register(new Scheme("https", ssf, 443)); registry.register(new Scheme("https", ssf, 443));
} catch (KeyManagementException ex) { } catch (KeyManagementException | NoSuchAlgorithmException ex) {
throw new RuntimeException(ex);
} catch (NoSuchAlgorithmException ex) {
throw new RuntimeException(ex); throw new RuntimeException(ex);
} }
} }

View File

@ -55,9 +55,8 @@ public class ConnectTask implements Callable<MinecraftClient> {
try { try {
log.debug("Pausing for {} ms", this.connectOptions.getTimeBetweenRetries().toMillis()); log.debug("Pausing for {} ms", this.connectOptions.getTimeBetweenRetries().toMillis());
Thread.sleep(this.connectOptions.getTimeBetweenRetries().toMillis()); Thread.sleep(this.connectOptions.getTimeBetweenRetries().toMillis());
} catch (InterruptedException var2) { } catch (InterruptedException e) {
InterruptedException e = var2; e.printStackTrace();
e.printStackTrace();
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }

View File

@ -3,6 +3,7 @@ package com.linearpast.minecraftmanager.utils.rcon;
import com.linearpast.minecraftmanager.utils.WhitelistTarget; import com.linearpast.minecraftmanager.utils.WhitelistTarget;
import com.linearpast.minecraftmanager.utils.config.SelfConfig; import com.linearpast.minecraftmanager.utils.config.SelfConfig;
import io.graversen.minecraft.rcon.commands.base.ICommand; import io.graversen.minecraft.rcon.commands.base.ICommand;
import io.graversen.minecraft.rcon.util.Target;
import io.graversen.minecraft.rcon.util.WhiteListModes; import io.graversen.minecraft.rcon.util.WhiteListModes;
import lombok.Getter; import lombok.Getter;
import org.apache.commons.text.StringSubstitutor; import org.apache.commons.text.StringSubstitutor;
@ -10,19 +11,25 @@ import org.apache.commons.text.StringSubstitutor;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
public record LoginWhitelistCommand(WhitelistTarget whitelistTarget, WhiteListModes whiteListMode) implements ICommand { //public record LoginWhitelistCommand(WhitelistTarget whitelistTarget, WhiteListModes whiteListMode) implements ICommand {
public LoginWhitelistCommand(WhitelistTarget whitelistTarget, WhiteListModes whiteListMode) { // public LoginWhitelistCommand(WhitelistTarget whitelistTarget, WhiteListModes whiteListMode) {
this.whitelistTarget = whitelistTarget; // this.whitelistTarget = whitelistTarget;
this.whiteListMode = Objects.requireNonNull(whiteListMode); // this.whiteListMode = Objects.requireNonNull(whiteListMode);
} // }
//
// public String command() {
// return switch (this.whiteListMode()) {
// case ADD -> StringSubstitutor.replace(SelfConfig.addCommand + " ${name} ${uuid}", Map.of(
// "name", this.whitelistTarget().name(),
// "uuid", this.whitelistTarget().uuid())
// );
// case REMOVE, LIST, OFF, ON, RELOAD -> "";
// };
// }
//}
public String command() { public class LoginWhitelistCommand extends SelfWhiteListCommand {
return switch (this.whiteListMode()) { public LoginWhitelistCommand(WhitelistTarget target, WhiteListModes whiteListMode) {
case ADD -> StringSubstitutor.replace(SelfConfig.addCommand + " ${name} ${uuid}", Map.of( super(Target.player(target.name()), whiteListMode);
"name", this.whitelistTarget().name(),
"uuid", this.whitelistTarget().uuid())
);
case REMOVE, LIST, OFF, ON, RELOAD -> "";
};
} }
} }

View File

@ -12,6 +12,7 @@ import java.util.concurrent.*;
import io.graversen.minecraft.rcon.service.ConnectOptions; import io.graversen.minecraft.rcon.service.ConnectOptions;
import io.graversen.minecraft.rcon.service.*; import io.graversen.minecraft.rcon.service.*;
import lombok.Setter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -22,7 +23,8 @@ public class MinecraftRconUtils {
private final ScheduledExecutorService executorService; private final ScheduledExecutorService executorService;
private volatile IMinecraftClient minecraftClient; private volatile IMinecraftClient minecraftClient;
private volatile MinecraftRcon minecraftRcon; private volatile MinecraftRcon minecraftRcon;
private volatile boolean isConnected; @Setter
private volatile boolean isConnected;
private volatile CountDownLatch connectionLatch; private volatile CountDownLatch connectionLatch;
public MinecraftRconUtils(RconDetails rconDetails, ConnectOptions connectOptions) { public MinecraftRconUtils(RconDetails rconDetails, ConnectOptions connectOptions) {
@ -108,11 +110,8 @@ public class MinecraftRconUtils {
if(this.minecraftClient != null) this.minecraftRcon = new MinecraftRcon(this.minecraftClient); if(this.minecraftClient != null) this.minecraftRcon = new MinecraftRcon(this.minecraftClient);
else this.minecraftRcon = null; else this.minecraftRcon = null;
} }
public void setConnected(boolean connected) {
this.isConnected = connected;
}
private class TestConnect implements Runnable { private class TestConnect implements Runnable {
private final RconDetails rconDetails; private final RconDetails rconDetails;
TestConnect(RconDetails rconDetails) { TestConnect(RconDetails rconDetails) {
this.rconDetails = rconDetails; this.rconDetails = rconDetails;

View File

@ -9,11 +9,8 @@ spring:
name: MinecraftManager name: MinecraftManager
datasource: datasource:
url: jdbc:mysql://127.0.0.1:3306/minecraft_manager_ltd?useSSL=false url: jdbc:mysql://127.0.0.1:3306/minecraft_manager_ltd?useSSL=false
username: root username: admin
password: 3235566389lzh password: 123456
# url: jdbc:mysql://127.0.0.1:3308/minecraft_manager_ltd?useSSL=false
# username: leisuretimedock
# password: ltd20250706
driver-class-name: com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver
jpa: jpa:
show-sql: true show-sql: true
@ -29,8 +26,8 @@ spring:
auth: true auth: true
ssl: ssl:
enable: true enable: true
password: 1111 username: admin@163.com
username: email@163.com password: 123456789
port: 465 port: 465
host: smtp.163.com host: smtp.163.com
@ -44,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);
}
}