feat(policy): velocity plugin sending a lovisual:policy payload
This commit is contained in:
parent
af2b417289
commit
711fcafc3a
9 changed files with 543 additions and 0 deletions
52
server-plugin/build.gradle
Normal file
52
server-plugin/build.gradle
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
}
|
||||||
|
|
||||||
|
group = 'dev.loki.lovisual'
|
||||||
|
version = '1.0.0'
|
||||||
|
|
||||||
|
java {
|
||||||
|
// velocity-api 4.2.0 class files require Java 25; settings.gradle enables
|
||||||
|
// foojay auto-provisioning so a plain JDK 21 machine still builds.
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(25)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
name = 'papermc'
|
||||||
|
url = 'https://repo.papermc.io/repository/maven-public/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly 'com.velocitypowered:velocity-api:4.2.0'
|
||||||
|
annotationProcessor 'com.velocitypowered:velocity-api:4.2.0'
|
||||||
|
|
||||||
|
implementation 'com.google.code.gson:gson:2.14.0'
|
||||||
|
implementation 'org.yaml:snakeyaml:2.5'
|
||||||
|
|
||||||
|
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4'
|
||||||
|
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||||
|
testImplementation 'com.google.code.gson:gson:2.14.0'
|
||||||
|
testImplementation 'org.yaml:snakeyaml:2.5'
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named('test') {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single deployable jar: plugin classes + shaded gson/snakeyaml + the
|
||||||
|
// annotation-processor generated velocity-plugin.json from processResources.
|
||||||
|
tasks.named('jar', Jar) {
|
||||||
|
archiveBaseName = 'lovisual-policy'
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
|
exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA', 'META-INF/MANIFEST.MF'
|
||||||
|
exclude 'module-info.class', 'META-INF/versions/**/module-info.class'
|
||||||
|
exclude 'META-INF/maven/**', 'META-INF/proguard/**', 'META-INF/LICENSE*', 'META-INF/NOTICE*'
|
||||||
|
from {
|
||||||
|
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
5
server-plugin/settings.gradle
Normal file
5
server-plugin/settings.gradle
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
plugins {
|
||||||
|
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = 'lovisual-policy'
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
package dev.loki.lovisual.policy;
|
||||||
|
|
||||||
|
import org.yaml.snakeyaml.Yaml;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The proxy-side policy: which mod features are denied and how strictly.
|
||||||
|
* Lives in {@code plugins/lovisual-policy/config.yml}; written with defaults
|
||||||
|
* on first run. Invalid content (e.g. an unknown mode) throws so the plugin
|
||||||
|
* can log the error instead of silently pushing a broken policy.
|
||||||
|
*/
|
||||||
|
public record PolicyConfig(boolean enabled, PolicyMode mode, String reason, List<String> deny) {
|
||||||
|
|
||||||
|
private static final String DEFAULT_REASON = "Правила сервера";
|
||||||
|
private static final List<String> EXAMPLE_DENY =
|
||||||
|
List.of("esp", "freecam", "hitboxes", "fullbright");
|
||||||
|
|
||||||
|
static final String DEFAULT_YAML = """
|
||||||
|
# LoVisual server policy: features denied to clients on this proxy.
|
||||||
|
# mode: lock — denied modules are switched off and cannot be enabled;
|
||||||
|
# mode: suggest — the mod only warns the player.
|
||||||
|
enabled: true
|
||||||
|
mode: lock
|
||||||
|
reason: "Правила сервера"
|
||||||
|
deny:
|
||||||
|
- esp
|
||||||
|
- freecam
|
||||||
|
- hitboxes
|
||||||
|
- fullbright
|
||||||
|
""";
|
||||||
|
|
||||||
|
public PolicyConfig {
|
||||||
|
reason = reason == null ? "" : reason;
|
||||||
|
deny = deny == null ? List.of() : List.copyOf(deny);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PolicyConfig defaults() {
|
||||||
|
return new PolicyConfig(true, PolicyMode.LOCK, DEFAULT_REASON, EXAMPLE_DENY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads {@code config.yml}, writing {@link #DEFAULT_YAML} first when it is
|
||||||
|
* absent. An empty file falls back to {@link #defaults()}.
|
||||||
|
*/
|
||||||
|
public static PolicyConfig load(Path file) throws IOException {
|
||||||
|
if (!Files.exists(file)) {
|
||||||
|
Files.createDirectories(file.getParent());
|
||||||
|
Files.writeString(file, DEFAULT_YAML, StandardCharsets.UTF_8);
|
||||||
|
return defaults();
|
||||||
|
}
|
||||||
|
try (Reader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
|
||||||
|
Map<?, ?> map = new Yaml().load(reader);
|
||||||
|
if (map == null || map.isEmpty()) {
|
||||||
|
return defaults();
|
||||||
|
}
|
||||||
|
return fromMap(map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Builds a config from parsed YAML/map values with per-key fallbacks. */
|
||||||
|
public static PolicyConfig fromMap(Map<?, ?> map) {
|
||||||
|
if (map == null || map.isEmpty()) {
|
||||||
|
return defaults();
|
||||||
|
}
|
||||||
|
Object enabledRaw = map.get("enabled");
|
||||||
|
boolean enabled = enabledRaw == null
|
||||||
|
|| (enabledRaw instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(enabledRaw)));
|
||||||
|
PolicyMode mode = PolicyMode.parse(map.get("mode") == null ? null : String.valueOf(map.get("mode")));
|
||||||
|
Object reasonRaw = map.get("reason");
|
||||||
|
String reason = reasonRaw == null ? DEFAULT_REASON : String.valueOf(reasonRaw);
|
||||||
|
return new PolicyConfig(enabled, mode, reason, denyList(map.get("deny")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> denyList(Object raw) {
|
||||||
|
if (!(raw instanceof List<?> list)) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<String> out = new ArrayList<>(list.size());
|
||||||
|
for (Object item : list) {
|
||||||
|
if (item != null) {
|
||||||
|
out.add(String.valueOf(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package dev.loki.lovisual.policy;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.google.gson.JsonSyntaxException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The wire format of the {@code lovisual:policy} channel: UTF-8 JSON, one
|
||||||
|
* message per plugin-message packet. The mod only ever parses requests
|
||||||
|
* ({@link #isRequest}); the plugin only ever encodes policies
|
||||||
|
* ({@link #encodePolicy}).
|
||||||
|
*/
|
||||||
|
public final class PolicyMessage {
|
||||||
|
|
||||||
|
/** The registry name of the plugin channel shared with the mod. */
|
||||||
|
public static final String CHANNEL = "lovisual:policy";
|
||||||
|
|
||||||
|
private PolicyMessage() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Encodes a S→C policy payload. A null server name is sent as "". */
|
||||||
|
public static String encodePolicy(PolicyConfig config, String server) {
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("type", "policy");
|
||||||
|
json.addProperty("mode", config.mode() == PolicyMode.SUGGEST ? "suggest" : "lock");
|
||||||
|
json.addProperty("reason", config.reason());
|
||||||
|
json.addProperty("server", server == null ? "" : server);
|
||||||
|
JsonArray deny = new JsonArray();
|
||||||
|
for (String id : config.deny()) {
|
||||||
|
deny.add(id);
|
||||||
|
}
|
||||||
|
json.add("deny", deny);
|
||||||
|
return json.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when the given C→S payload is a policy request from the mod. */
|
||||||
|
public static boolean isRequest(String payload) {
|
||||||
|
if (payload == null || payload.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JsonObject json = JsonParser.parseString(payload).getAsJsonObject();
|
||||||
|
return json.has("type") && "request".equals(json.get("type").getAsString());
|
||||||
|
} catch (JsonSyntaxException | IllegalStateException | ClassCastException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package dev.loki.lovisual.policy;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How the mod must treat a denied feature: {@link #LOCK} refuses to enable it
|
||||||
|
* and shows a lock in the ClickGui, {@link #SUGGEST} only warns the player.
|
||||||
|
*/
|
||||||
|
public enum PolicyMode {
|
||||||
|
LOCK,
|
||||||
|
SUGGEST;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a config value. A missing value falls back to the conservative
|
||||||
|
* {@link #LOCK}; an unknown value is an admin error and throws.
|
||||||
|
*
|
||||||
|
* @throws IllegalArgumentException when {@code raw} is present but not {@code lock}/{@code suggest}
|
||||||
|
*/
|
||||||
|
public static PolicyMode parse(String raw) {
|
||||||
|
if (raw == null || raw.isBlank()) {
|
||||||
|
return LOCK;
|
||||||
|
}
|
||||||
|
return switch (raw.trim().toLowerCase(Locale.ROOT)) {
|
||||||
|
case "lock" -> LOCK;
|
||||||
|
case "suggest" -> SUGGEST;
|
||||||
|
default -> throw new IllegalArgumentException("unknown mode: " + raw + " (expected lock|suggest)");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
package dev.loki.lovisual.policy.plugin;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.command.CommandManager;
|
||||||
|
import com.velocitypowered.api.event.Subscribe;
|
||||||
|
import com.velocitypowered.api.event.connection.DisconnectEvent;
|
||||||
|
import com.velocitypowered.api.event.connection.PluginMessageEvent;
|
||||||
|
import com.velocitypowered.api.event.connection.PostLoginEvent;
|
||||||
|
import com.velocitypowered.api.event.player.ServerConnectedEvent;
|
||||||
|
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
|
||||||
|
import com.velocitypowered.api.plugin.Plugin;
|
||||||
|
import com.velocitypowered.api.plugin.annotation.DataDirectory;
|
||||||
|
import com.velocitypowered.api.proxy.ProxyServer;
|
||||||
|
import com.velocitypowered.api.proxy.Player;
|
||||||
|
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import dev.loki.lovisual.policy.PolicyConfig;
|
||||||
|
import dev.loki.lovisual.policy.PolicyMessage;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Velocity-side source of the {@code lovisual:policy} channel: loads
|
||||||
|
* {@code config.yml} once, pushes the policy on join/backend switch and in
|
||||||
|
* answer to the mod's {@code {"type":"request"}} message.
|
||||||
|
*/
|
||||||
|
@Plugin(id = "lovisual-policy", name = "LoVisual Policy", version = "1.0.0",
|
||||||
|
authors = {"loki"})
|
||||||
|
public final class LoVisualPolicyPlugin {
|
||||||
|
|
||||||
|
private static final MinecraftChannelIdentifier CHANNEL =
|
||||||
|
MinecraftChannelIdentifier.from(PolicyMessage.CHANNEL);
|
||||||
|
|
||||||
|
private final ProxyServer proxy;
|
||||||
|
private final Logger logger;
|
||||||
|
private final Path dataDirectory;
|
||||||
|
private final Map<UUID, String> currentServer = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/** Null when {@code config.yml} is invalid — then nothing is sent. */
|
||||||
|
private volatile PolicyConfig config;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public LoVisualPolicyPlugin(ProxyServer proxy, Logger logger, @DataDirectory Path dataDirectory) {
|
||||||
|
this.proxy = proxy;
|
||||||
|
this.logger = logger;
|
||||||
|
this.dataDirectory = dataDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onProxyInitialize(ProxyInitializeEvent event) {
|
||||||
|
proxy.getChannelRegistrar().register(CHANNEL);
|
||||||
|
CommandManager commands = proxy.getCommandManager();
|
||||||
|
commands.register(commands.metaBuilder("lvpolicy").plugin(this).build(),
|
||||||
|
new PolicyCommand(this));
|
||||||
|
reload();
|
||||||
|
logger.info("LoVisual policy loaded: {}", status());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onPostLogin(PostLoginEvent event) {
|
||||||
|
sendPolicy(event.getPlayer());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onServerConnected(ServerConnectedEvent event) {
|
||||||
|
currentServer.put(event.getPlayer().getUniqueId(),
|
||||||
|
event.getServer().getServerInfo().getName());
|
||||||
|
sendPolicy(event.getPlayer());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onDisconnect(DisconnectEvent event) {
|
||||||
|
currentServer.remove(event.getPlayer().getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onPluginMessage(PluginMessageEvent event) {
|
||||||
|
if (!CHANNEL.equals(event.getIdentifier())
|
||||||
|
|| !(event.getSource() instanceof Player player)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.setResult(PluginMessageEvent.ForwardResult.handled());
|
||||||
|
if (PolicyMessage.isRequest(new String(event.getData(), StandardCharsets.UTF_8))) {
|
||||||
|
sendPolicy(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Re-reads {@code config.yml} and pushes the fresh policy to everyone online. */
|
||||||
|
public void reload() {
|
||||||
|
try {
|
||||||
|
config = PolicyConfig.load(dataDirectory.resolve("config.yml"));
|
||||||
|
} catch (IOException | IllegalArgumentException e) {
|
||||||
|
config = null;
|
||||||
|
logger.error("Invalid config.yml, no policy will be sent until it is fixed", e);
|
||||||
|
}
|
||||||
|
for (Player player : proxy.getAllPlayers()) {
|
||||||
|
sendPolicy(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A one-line answer for {@code /lvpolicy status}. */
|
||||||
|
public String status() {
|
||||||
|
PolicyConfig cfg = config;
|
||||||
|
if (cfg == null) {
|
||||||
|
return "config invalid (see log), nothing is sent";
|
||||||
|
}
|
||||||
|
if (!cfg.enabled()) {
|
||||||
|
return "disabled";
|
||||||
|
}
|
||||||
|
return cfg.mode() + ", deny=" + cfg.deny().size() + " modules, reason=" + cfg.reason();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendPolicy(Player player) {
|
||||||
|
PolicyConfig cfg = config;
|
||||||
|
if (cfg == null || !cfg.enabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String server = currentServer.getOrDefault(player.getUniqueId(), "");
|
||||||
|
byte[] payload = PolicyMessage.encodePolicy(cfg, server).getBytes(StandardCharsets.UTF_8);
|
||||||
|
player.sendPluginMessage(CHANNEL, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package dev.loki.lovisual.policy.plugin;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.command.CommandSource;
|
||||||
|
import com.velocitypowered.api.command.SimpleCommand;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code /lvpolicy [reload|status]} — shows the active policy and re-reads
|
||||||
|
* {@code config.yml} without a proxy restart.
|
||||||
|
*/
|
||||||
|
final class PolicyCommand implements SimpleCommand {
|
||||||
|
|
||||||
|
private final LoVisualPolicyPlugin plugin;
|
||||||
|
|
||||||
|
PolicyCommand(LoVisualPolicyPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(Invocation invocation) {
|
||||||
|
CommandSource source = invocation.source();
|
||||||
|
String[] args = invocation.arguments();
|
||||||
|
String sub = args.length == 0 ? "status" : args[0];
|
||||||
|
if ("reload".equalsIgnoreCase(sub)) {
|
||||||
|
plugin.reload();
|
||||||
|
source.sendPlainMessage("LoVisual policy: " + plugin.status());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("status".equalsIgnoreCase(sub)) {
|
||||||
|
source.sendPlainMessage("LoVisual policy: " + plugin.status());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
source.sendPlainMessage("Использование: /lvpolicy [reload|status]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
package dev.loki.lovisual.policy;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class PolicyConfigTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void defaultsAreEnabledLockWithExampleDeny() {
|
||||||
|
PolicyConfig cfg = PolicyConfig.defaults();
|
||||||
|
assertTrue(cfg.enabled());
|
||||||
|
assertEquals(PolicyMode.LOCK, cfg.mode());
|
||||||
|
assertFalse(cfg.reason().isBlank());
|
||||||
|
assertFalse(cfg.deny().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromMapParsesAllFields() {
|
||||||
|
PolicyConfig cfg = PolicyConfig.fromMap(Map.of(
|
||||||
|
"enabled", true,
|
||||||
|
"mode", "suggest",
|
||||||
|
"reason", "custom rules",
|
||||||
|
"deny", List.of("esp", "freecam")
|
||||||
|
));
|
||||||
|
assertTrue(cfg.enabled());
|
||||||
|
assertEquals(PolicyMode.SUGGEST, cfg.mode());
|
||||||
|
assertEquals("custom rules", cfg.reason());
|
||||||
|
assertEquals(List.of("esp", "freecam"), cfg.deny());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromMapMissingModeDefaultsToLock() {
|
||||||
|
PolicyConfig cfg = PolicyConfig.fromMap(Map.of("deny", List.of("esp")));
|
||||||
|
assertEquals(PolicyMode.LOCK, cfg.mode());
|
||||||
|
assertEquals(List.of("esp"), cfg.deny());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromMapUnknownModeThrows() {
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> PolicyConfig.fromMap(Map.of("mode", "banana")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromMapDisabledWithEmptyDeny() {
|
||||||
|
PolicyConfig cfg = PolicyConfig.fromMap(Map.of(
|
||||||
|
"enabled", false,
|
||||||
|
"mode", "lock",
|
||||||
|
"deny", List.of()
|
||||||
|
));
|
||||||
|
assertFalse(cfg.enabled());
|
||||||
|
assertTrue(cfg.deny().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadWritesDefaultConfigWhenMissing(@TempDir Path dir) throws Exception {
|
||||||
|
Path file = dir.resolve("config.yml");
|
||||||
|
PolicyConfig cfg = PolicyConfig.load(file);
|
||||||
|
assertTrue(Files.exists(file));
|
||||||
|
assertEquals(PolicyConfig.defaults(), cfg);
|
||||||
|
assertEquals(cfg, PolicyConfig.load(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadParsesYamlFile(@TempDir Path dir) throws Exception {
|
||||||
|
Path file = dir.resolve("config.yml");
|
||||||
|
Files.writeString(file, """
|
||||||
|
enabled: false
|
||||||
|
mode: suggest
|
||||||
|
reason: "no cheats"
|
||||||
|
deny:
|
||||||
|
- esp
|
||||||
|
- hitboxes
|
||||||
|
""");
|
||||||
|
PolicyConfig cfg = PolicyConfig.load(file);
|
||||||
|
assertFalse(cfg.enabled());
|
||||||
|
assertEquals(PolicyMode.SUGGEST, cfg.mode());
|
||||||
|
assertEquals("no cheats", cfg.reason());
|
||||||
|
assertEquals(List.of("esp", "hitboxes"), cfg.deny());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadThrowsOnUnknownMode(@TempDir Path dir) throws Exception {
|
||||||
|
Path file = dir.resolve("config.yml");
|
||||||
|
Files.writeString(file, "mode: banana\n");
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> PolicyConfig.load(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadOfEmptyFileFallsBackToDefaults(@TempDir Path dir) throws Exception {
|
||||||
|
Path file = dir.resolve("config.yml");
|
||||||
|
Files.writeString(file, "");
|
||||||
|
assertEquals(PolicyConfig.defaults(), PolicyConfig.load(file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package dev.loki.lovisual.policy;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class PolicyMessageTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encodePolicyWritesAllFields() {
|
||||||
|
PolicyConfig cfg = new PolicyConfig(true, PolicyMode.SUGGEST, "server rules",
|
||||||
|
List.of("esp", "freecam"));
|
||||||
|
String json = PolicyMessage.encodePolicy(cfg, "lobby-1");
|
||||||
|
JsonObject parsed = JsonParser.parseString(json).getAsJsonObject();
|
||||||
|
assertEquals("policy", parsed.get("type").getAsString());
|
||||||
|
assertEquals("suggest", parsed.get("mode").getAsString());
|
||||||
|
assertEquals("server rules", parsed.get("reason").getAsString());
|
||||||
|
assertEquals("lobby-1", parsed.get("server").getAsString());
|
||||||
|
assertEquals(2, parsed.getAsJsonArray("deny").size());
|
||||||
|
assertEquals("esp", parsed.getAsJsonArray("deny").get(0).getAsString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encodePolicyEscapesReasonAndDefaultsServerToEmpty() {
|
||||||
|
PolicyConfig cfg = new PolicyConfig(true, PolicyMode.LOCK, "say \"hi\"\nline2", List.of());
|
||||||
|
JsonObject parsed = JsonParser.parseString(PolicyMessage.encodePolicy(cfg, null)).getAsJsonObject();
|
||||||
|
assertEquals("say \"hi\"\nline2", parsed.get("reason").getAsString());
|
||||||
|
assertEquals("", parsed.get("server").getAsString());
|
||||||
|
assertEquals(0, parsed.getAsJsonArray("deny").size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isRequestAcceptsOnlyTypedRequests() {
|
||||||
|
assertTrue(PolicyMessage.isRequest("{\"type\":\"request\"}"));
|
||||||
|
assertTrue(PolicyMessage.isRequest(" { \"type\" : \"request\" } "));
|
||||||
|
assertFalse(PolicyMessage.isRequest("{\"type\":\"policy\"}"));
|
||||||
|
assertFalse(PolicyMessage.isRequest("{\"type\":42}"));
|
||||||
|
assertFalse(PolicyMessage.isRequest("garbage"));
|
||||||
|
assertFalse(PolicyMessage.isRequest(""));
|
||||||
|
assertFalse(PolicyMessage.isRequest(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue