Unverified Commit 9a238780 authored by Lemon-miaow's avatar Lemon-miaow
Browse files

test(plugins): 登录闸门、大厅菜单和 mod /link 抽成可测类并补行为测试

parent 6d5aca79
Loading
Loading
Loading
Loading
+7 −2
Changes for plugins/README.md: 7 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -326,8 +326,13 @@ minutes; subsequent builds are fast. Jars land in each module's `build/libs`. CI
both gates: `bash plugins/test.sh` (JDK 25 — the install-time plugins, the
codec/invite/server-list tests, and the proxy routing tests that drive ServerRegistry,
WaitingRouter and ControlChannel against the real velocity-api; run those alone with
`plugins/velocity/gradlew -p plugins/velocity routingTest`) and `bash plugins/test-mods.sh` (JDK 17 — the three
loader mods, via the wrappers above).
`plugins/velocity/gradlew -p plugins/velocity routingTest`. It also runs the lobby menu
test that drives LobbyMenu against the real paper-api (`plugins/paper/gradlew -p
plugins/paper lobbyTest`), the login gate test that drives LoginFlow against a stub
felis-api on virtual ticks (`plugins/limbo/gradlew -p plugins/limbo
-PlimboVersion=<lock's LIMBO_VERSION> loginTest`), and ModLinkTest for the `/link` the
loader mods share) and `bash plugins/test-mods.sh` (JDK 17 — the three loader mods,
via the wrappers above).

### Dependency verification

+45 −44
Changes for plugins/fabric/src/main/java/best/lolicon/felis/fabric/FelisFabricMod.java: 45 added lines, 44 removed lines.
Original line number Diff line number Diff line
package best.lolicon.felis.fabric;

import best.lolicon.felis.link.LinkClient;
import best.lolicon.felis.link.LinkCode;
import best.lolicon.felis.link.LinkConfig;
import best.lolicon.felis.link.LinkConfigLoader;
import best.lolicon.felis.link.LinkException;
import best.lolicon.felis.link.ModLinkPolicy;
import best.lolicon.felis.link.ModLink;

import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
@@ -16,40 +14,39 @@ import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import org.slf4j.Logger;

import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

/**
 * FelisFabricMod is the Fabric (dedicated-server) leg of the §10 account-link
 * flow. A server-side {@code /link} command takes the player's already-verified
 * UUID, asks felis-api for a one-time code, and shows it in chat; the player then
 * redeems it on the web console. The HTTP call is pushed onto a daemon I/O thread
 * and the reply is hopped back onto the server thread, so a slow felis-api never
 * stalls the tick loop. Failures collapse to a generic chat line with details
 * confined to the server log.
 * redeems it on the web console. The command itself is the shared {@link ModLink}
 * (the HTTP call on a daemon I/O thread, the reply back on the server thread, one
 * generic line for any failure); this class registers it and adapts the command
 * source.
 */
public final class FelisFabricMod implements DedicatedServerModInitializer {
    private static final Logger LOGGER = LogUtils.getLogger();

    private final ExecutorService io = Executors.newSingleThreadExecutor(r -> {
    private final Executor io = Executors.newSingleThreadExecutor(r -> {
        Thread t = new Thread(r, "felis-link-io");
        t.setDaemon(true);
        return t;
    });
    private LinkClient linkClient;
    private ModLink link;

    @Override
    public void onInitializeServer() {
        try {
            LinkConfig config = LinkConfigLoader.load(
                    FabricLoader.getInstance().getConfigDir().resolve("felis-link.properties"));
            this.linkClient = new LinkClient(config);
            this.link = new ModLink(new LinkClient(config), io, LOGGER::warn);
        } catch (IOException e) {
            LOGGER.error("Felis link disabled: {}", e.getMessage());
            return;
@@ -60,44 +57,48 @@ public final class FelisFabricMod implements DedicatedServerModInitializer {
    }

    private void register(CommandDispatcher<CommandSourceStack> dispatcher) {
        dispatcher.register(Commands.literal("link").executes(ctx -> {
            CommandSourceStack source = ctx.getSource();
            ServerPlayer player;
        dispatcher.register(Commands.literal("link").executes(ctx -> link.run(new LinkSource(ctx.getSource()))));
    }

    /** LinkSource is a command source as the shared /link sees it. */
    private static final class LinkSource implements ModLink.Source {
        private final CommandSourceStack source;
        private final ServerPlayer player; // null for the console or a command block

        LinkSource(CommandSourceStack source) {
            this.source = source;
            ServerPlayer p;
            try {
                player = source.getPlayerOrException();
                p = source.getPlayerOrException();
            } catch (CommandSyntaxException e) {
                source.sendFailure(Component.literal("/link 只能由玩家执行 / /link can only be run by a player."));
                return 0;
                p = null;
            }
            if (!source.getServer().usesAuthentication()) {
                LOGGER.warn(ModLinkPolicy.OFFLINE_LOG);
                source.sendFailure(Component.literal(ModLinkPolicy.OFFLINE_REPLY));
                return 0;
            this.player = p;
        }
            requestAndReply(source.getServer(), player);
            return 1;
        }));

        @Override
        public UUID player() {
            return player != null ? player.getUUID() : null;
        }

    private void requestAndReply(MinecraftServer server, ServerPlayer player) {
        UUID uuid = player.getUUID();
        player.sendSystemMessage(Component.literal("正在获取绑定码… / Requesting a link code…"));
        io.submit(() -> {
            try {
                LinkCode code = linkClient.requestCode(uuid);
                server.execute(() -> {
                    player.sendSystemMessage(Component.literal(
                            "绑定码 / Link code: " + code.code() + "(几分钟内有效 / valid a few minutes)"));
                    player.sendSystemMessage(Component.literal(code.panelUrl() != null
                            ? "在此完成绑定 / Finish linking at: " + code.panelUrl()
                            : "在网页控制台 → 账户 中输入 / Enter it on the web console → Account."));
                });
            } catch (LinkException e) {
                LOGGER.warn("link code request failed for {} (status={}, code={}): {}",
                        uuid, e.statusCode(), e.errorCode(), e.getMessage());
                server.execute(() -> player.sendSystemMessage(Component.literal(
                        "现在无法获取绑定码,请稍后再试 / Couldn't get a link code right now. Please try again in a moment.")));
        @Override
        public boolean onlineMode() {
            return source.getServer().usesAuthentication();
        }

        @Override
        public void fail(String line) {
            source.sendFailure(Component.literal(line));
        }

        @Override
        public void tell(String line) {
            player.sendSystemMessage(Component.literal(line));
        }

        @Override
        public void onServerThread(Runnable task) {
            source.getServer().execute(task);
        }
        });
    }
}
+45 −44
Changes for plugins/forge/src/main/java/best/lolicon/felis/forge/FelisForgeMod.java: 45 added lines, 44 removed lines.
Original line number Diff line number Diff line
package best.lolicon.felis.forge;

import best.lolicon.felis.link.LinkClient;
import best.lolicon.felis.link.LinkCode;
import best.lolicon.felis.link.LinkConfig;
import best.lolicon.felis.link.LinkConfigLoader;
import best.lolicon.felis.link.LinkException;
import best.lolicon.felis.link.ModLinkPolicy;
import best.lolicon.felis.link.ModLink;

import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
@@ -13,7 +11,6 @@ import com.mojang.logging.LogUtils;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.RegisterCommandsEvent;
@@ -24,34 +21,34 @@ import org.slf4j.Logger;

import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

/**
 * FelisForgeMod is the Forge server-side leg of the §10 account-link flow. On
 * {@link RegisterCommandsEvent} it installs a {@code /link} command that mints a
 * one-time code from felis-api for the player's already-verified UUID. As on the
 * other loaders, the HTTP call runs on a daemon I/O thread and the reply is hopped
 * back onto the server thread; failures collapse to a generic chat line with
 * details kept to the server log. If config is missing the mod stays loaded but
 * one-time code from felis-api for the player's already-verified UUID. The command
 * is the shared {@link ModLink}, as on the other loaders (the HTTP call on a daemon
 * I/O thread, the reply back on the server thread, one generic line for any
 * failure). If config is missing the mod stays loaded but
 * never registers the command, so the proxy/server runs un-crippled.
 */
@Mod("felis_link")
public final class FelisForgeMod {
    private static final Logger LOGGER = LogUtils.getLogger();

    private final ExecutorService io = Executors.newSingleThreadExecutor(r -> {
    private final Executor io = Executors.newSingleThreadExecutor(r -> {
        Thread t = new Thread(r, "felis-link-io");
        t.setDaemon(true);
        return t;
    });
    private LinkClient linkClient;
    private ModLink link;

    public FelisForgeMod() {
        try {
            LinkConfig config = LinkConfigLoader.load(
                    FMLPaths.CONFIGDIR.get().resolve("felis-link.properties"));
            this.linkClient = new LinkClient(config);
            this.link = new ModLink(new LinkClient(config), io, LOGGER::warn);
            MinecraftForge.EVENT_BUS.register(this);
            LOGGER.info("Felis link ready; /link will be registered.");
        } catch (IOException e) {
@@ -65,44 +62,48 @@ public final class FelisForgeMod {
    }

    private void register(CommandDispatcher<CommandSourceStack> dispatcher) {
        dispatcher.register(Commands.literal("link").executes(ctx -> {
            CommandSourceStack source = ctx.getSource();
            ServerPlayer player;
        dispatcher.register(Commands.literal("link").executes(ctx -> link.run(new LinkSource(ctx.getSource()))));
    }

    /** LinkSource is a command source as the shared /link sees it. */
    private static final class LinkSource implements ModLink.Source {
        private final CommandSourceStack source;
        private final ServerPlayer player; // null for the console or a command block

        LinkSource(CommandSourceStack source) {
            this.source = source;
            ServerPlayer p;
            try {
                player = source.getPlayerOrException();
                p = source.getPlayerOrException();
            } catch (CommandSyntaxException e) {
                source.sendFailure(Component.literal("/link 只能由玩家执行 / /link can only be run by a player."));
                return 0;
                p = null;
            }
            if (!source.getServer().usesAuthentication()) {
                LOGGER.warn(ModLinkPolicy.OFFLINE_LOG);
                source.sendFailure(Component.literal(ModLinkPolicy.OFFLINE_REPLY));
                return 0;
            this.player = p;
        }
            requestAndReply(source.getServer(), player);
            return 1;
        }));

        @Override
        public UUID player() {
            return player != null ? player.getUUID() : null;
        }

    private void requestAndReply(MinecraftServer server, ServerPlayer player) {
        UUID uuid = player.getUUID();
        player.sendSystemMessage(Component.literal("正在获取绑定码… / Requesting a link code…"));
        io.submit(() -> {
            try {
                LinkCode code = linkClient.requestCode(uuid);
                server.execute(() -> {
                    player.sendSystemMessage(Component.literal(
                            "绑定码 / Link code: " + code.code() + "(几分钟内有效 / valid a few minutes)"));
                    player.sendSystemMessage(Component.literal(code.panelUrl() != null
                            ? "在此完成绑定 / Finish linking at: " + code.panelUrl()
                            : "在网页控制台 → 账户 中输入 / Enter it on the web console → Account."));
                });
            } catch (LinkException e) {
                LOGGER.warn("link code request failed for {} (status={}, code={}): {}",
                        uuid, e.statusCode(), e.errorCode(), e.getMessage());
                server.execute(() -> player.sendSystemMessage(Component.literal(
                        "现在无法获取绑定码,请稍后再试 / Couldn't get a link code right now. Please try again in a moment.")));
        @Override
        public boolean onlineMode() {
            return source.getServer().usesAuthentication();
        }

        @Override
        public void fail(String line) {
            source.sendFailure(Component.literal(line));
        }

        @Override
        public void tell(String line) {
            player.sendSystemMessage(Component.literal(line));
        }

        @Override
        public void onServerThread(Runnable task) {
            source.getServer().execute(task);
        }
        });
    }
}
+23 −0
Changes for plugins/limbo/build.gradle: 23 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -58,3 +58,26 @@ tasks.withType(JavaCompile).configureEach {
    // bytecode so the plugin loads on any Limbo running Java 17+.
    options.release = 17
}

// The login gate's self-test: LoginFlowTest runs the whole login flow on a virtual
// clock (a fake scheduler and players around the real LoginFlow, the real felis-api
// clients against a stub) plus the readiness endpoint, the env parsing and the book.
// A framework-free main like the other plugins' tests; plugins/test.sh runs
// `./gradlew loginTest`. The Limbo jar is on the classpath for the adventure types the
// book is built from.
sourceSets {
    loginTest {
        java {
            srcDir 'test'
        }
        compileClasspath += sourceSets.main.output + configurations.compileClasspath
        runtimeClasspath += output + compileClasspath
    }
}

tasks.register('loginTest', JavaExec) {
    group = 'verification'
    description = 'Runs the LoginFlowTest self-test main.'
    classpath = sourceSets.loginTest.runtimeClasspath
    mainClass = 'best.lolicon.felis.limbo.LoginFlowTest'
}
+91 −366

File changed.

Preview size limit exceeded, changes collapsed.

Loading