From 03c60fcd3cfd67d7210ffc0e67f702cab55ddc90 Mon Sep 17 00:00:00 2001 From: James Mortemore Date: Fri, 3 Apr 2026 16:15:58 +0100 Subject: [PATCH 01/10] feat: replace [pin] on initial kick and fix pinCheck boot warning Listen to PlayerBannedEvent on all 6 platforms to call handlePin when a kickMessage is present, enabling [pin] replacement on the first kick of an online player (not just reconnect denial). Change ExpiresSync from extends BmRunnable to implements Runnable to eliminate the spurious "unknown last checked pinCheck" log on every boot. Add e2e tests verifying the kick message contains a 6-digit pin when banning/tempbanning an online player. Add waitForKick() helper to TestBot with proper listener cleanup and race-safe settlement. --- .../bukkit/listeners/ReportListener.java | 13 ++++ .../bungee/listeners/BanListener.java | 13 ++++ .../common/runnables/ExpiresSync.java | 6 +- e2e/tests/src/denied-pin.test.ts | 70 +++++++++++++++++-- e2e/tests/src/helpers/bot.ts | 37 ++++++++++ .../fabric/listeners/EventListener.java | 9 ++- .../sponge/listeners/ReportListener.java | 13 ++++ .../sponge/listeners/ReportListener.java | 13 ++++ .../velocity/listener/BanListener.java | 13 ++++ 9 files changed, 177 insertions(+), 10 deletions(-) diff --git a/bukkit/src/main/java/me/confuser/banmanager/webenhancer/bukkit/listeners/ReportListener.java b/bukkit/src/main/java/me/confuser/banmanager/webenhancer/bukkit/listeners/ReportListener.java index 600af19..8dac0ea 100644 --- a/bukkit/src/main/java/me/confuser/banmanager/webenhancer/bukkit/listeners/ReportListener.java +++ b/bukkit/src/main/java/me/confuser/banmanager/webenhancer/bukkit/listeners/ReportListener.java @@ -1,10 +1,12 @@ package me.confuser.banmanager.webenhancer.bukkit.listeners; import me.confuser.banmanager.common.ormlite.stmt.DeleteBuilder; +import me.confuser.banmanager.bukkit.api.events.PlayerBannedEvent; import me.confuser.banmanager.bukkit.api.events.PlayerReportDeletedEvent; import me.confuser.banmanager.bukkit.api.events.PlayerReportedEvent; import me.confuser.banmanager.bukkit.api.events.PlayerDeniedEvent; import me.confuser.banmanager.bukkit.api.events.PluginReloadedEvent; +import me.confuser.banmanager.common.util.Message; import me.confuser.banmanager.common.data.PlayerReportData; import me.confuser.banmanager.webenhancer.bukkit.BukkitPlugin; import me.confuser.banmanager.webenhancer.common.data.LogData; @@ -76,6 +78,17 @@ public void onDeny(PlayerDeniedEvent event) { listener.handlePin(event.getPlayer(), event.getMessage()); } + @EventHandler(priority = EventPriority.MONITOR) + public void onBanned(PlayerBannedEvent event) { + try { + Message kickMessage = event.getKickMessage(); + if (kickMessage != null) { + listener.handlePin(event.getBan().getPlayer(), kickMessage); + } + } catch (NoSuchMethodError ignored) { + } + } + @EventHandler public void onReload(PluginReloadedEvent event) { plugin.getPlugin().setupConfigs(); diff --git a/bungee/src/main/java/me/confuser/banmanager/webenhancer/bungee/listeners/BanListener.java b/bungee/src/main/java/me/confuser/banmanager/webenhancer/bungee/listeners/BanListener.java index ae1ae9e..6f0e91f 100644 --- a/bungee/src/main/java/me/confuser/banmanager/webenhancer/bungee/listeners/BanListener.java +++ b/bungee/src/main/java/me/confuser/banmanager/webenhancer/bungee/listeners/BanListener.java @@ -1,7 +1,9 @@ package me.confuser.banmanager.webenhancer.bungee.listeners; +import me.confuser.banmanager.bungee.api.events.PlayerBannedEvent; import me.confuser.banmanager.bungee.api.events.PlayerDeniedEvent; import me.confuser.banmanager.bungee.api.events.PluginReloadedEvent; +import me.confuser.banmanager.common.util.Message; import me.confuser.banmanager.webenhancer.bungee.BungeePlugin; import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.event.EventHandler; @@ -21,6 +23,17 @@ public void onDeny(PlayerDeniedEvent event) { listener.handlePin(event.getPlayer(), event.getMessage()); } + @EventHandler + public void onBanned(PlayerBannedEvent event) { + try { + Message kickMessage = event.getKickMessage(); + if (kickMessage != null) { + listener.handlePin(event.getBan().getPlayer(), kickMessage); + } + } catch (NoSuchMethodError ignored) { + } + } + @EventHandler public void onReload(PluginReloadedEvent event) { plugin.getPlugin().setupConfigs(); diff --git a/common/src/main/java/me/confuser/banmanager/webenhancer/common/runnables/ExpiresSync.java b/common/src/main/java/me/confuser/banmanager/webenhancer/common/runnables/ExpiresSync.java index 849865a..a8d522a 100644 --- a/common/src/main/java/me/confuser/banmanager/webenhancer/common/runnables/ExpiresSync.java +++ b/common/src/main/java/me/confuser/banmanager/webenhancer/common/runnables/ExpiresSync.java @@ -1,8 +1,6 @@ package me.confuser.banmanager.webenhancer.common.runnables; import me.confuser.banmanager.common.ormlite.stmt.DeleteBuilder; -import me.confuser.banmanager.common.BanManagerPlugin; -import me.confuser.banmanager.common.runnables.BmRunnable; import me.confuser.banmanager.common.util.DateUtils; import me.confuser.banmanager.webenhancer.common.WebEnhancerPlugin; import me.confuser.banmanager.webenhancer.common.data.PlayerPinData; @@ -10,12 +8,10 @@ import java.sql.SQLException; -public class ExpiresSync extends BmRunnable { +public class ExpiresSync implements Runnable { private PlayerPinStorage pinStorage; public ExpiresSync(WebEnhancerPlugin plugin) { - super(BanManagerPlugin.getInstance(), "pinCheck"); - pinStorage = plugin.getPlayerPinStorage(); } diff --git a/e2e/tests/src/denied-pin.test.ts b/e2e/tests/src/denied-pin.test.ts index 4242b16..10887b7 100644 --- a/e2e/tests/src/denied-pin.test.ts +++ b/e2e/tests/src/denied-pin.test.ts @@ -1,5 +1,9 @@ import { TestBot, createBot, sendCommand, disconnectRcon, sleep } from './helpers' +afterAll(async () => { + await disconnectRcon() +}) + describe('Denied Pin Placeholder', () => { let bannedBot: TestBot | null = null const BANNED_USERNAME = 'DeniedPinPlayer' @@ -37,10 +41,6 @@ describe('Denied Pin Placeholder', () => { bannedBot = null }) - afterAll(async () => { - await disconnectRcon() - }) - test('[pin] placeholder in ban message is replaced with actual pin', async () => { const banResponse = await sendCommand(`bmban ${BANNED_USERNAME} Testing pin placeholder`) console.log(`Ban response: ${banResponse}`) @@ -82,3 +82,65 @@ describe('Denied Pin Placeholder', () => { } }, TEST_TIMEOUT_MS) }) + +describe('Online Kick Pin Placeholder', () => { + const KICK_USERNAME = 'OnlineKickPin' + const TEST_TIMEOUT_MS = 60000 + let bot: TestBot | null = null + + afterEach(async () => { + try { + await sendCommand(`bmunban ${KICK_USERNAME}`) + } catch (e) {} + await bot?.disconnect() + bot = null + }) + + test('[pin] placeholder in kick message is replaced when banning an online player', async () => { + bot = await createBot(KICK_USERNAME) + const kickPromise = bot.waitForKick() + + await sleep(1000) + const banResponse = await sendCommand(`bmban ${KICK_USERNAME} Testing online kick pin`) + console.log(`Ban response: ${banResponse}`) + + const kickReason = await kickPromise + console.log(`Online player kicked with reason: ${kickReason}`) + bot = null + + expect(kickReason).not.toContain('[pin]') + + const pinMatch = kickReason.match(/pin is (\d{6})/) + expect(pinMatch).not.toBeNull() + + if (pinMatch != null) { + const pin = pinMatch[1] + console.log(`Extracted pin from online kick message: ${pin}`) + expect(pin).toMatch(/^\d{6}$/) + } + }, TEST_TIMEOUT_MS) + + test('[pin] placeholder in kick message is replaced when tempbanning an online player', async () => { + bot = await createBot(KICK_USERNAME) + const kickPromise = bot.waitForKick() + + await sleep(1000) + const tempbanResponse = await sendCommand(`bmtempban ${KICK_USERNAME} 1h Testing online tempban kick pin`) + console.log(`Tempban response: ${tempbanResponse}`) + + const kickReason = await kickPromise + console.log(`Online player kicked (tempban) with reason: ${kickReason}`) + bot = null + + expect(kickReason).not.toContain('[pin]') + + const pinMatch = kickReason.match(/pin is (\d{6})/) + expect(pinMatch).not.toBeNull() + + if (pinMatch != null) { + const pin = pinMatch[1] + console.log(`Extracted pin from online tempban kick message: ${pin}`) + expect(pin).toMatch(/^\d{6}$/) + } + }, TEST_TIMEOUT_MS) +}) diff --git a/e2e/tests/src/helpers/bot.ts b/e2e/tests/src/helpers/bot.ts index 6517c91..bc13589 100644 --- a/e2e/tests/src/helpers/bot.ts +++ b/e2e/tests/src/helpers/bot.ts @@ -288,6 +288,43 @@ export class TestBot { } } + async waitForKick (timeoutMs: number = 30000): Promise { + if (this.bot == null) { + throw new Error('Bot not connected') + } + + const bot = this.bot + bot.removeAllListeners('kicked') + + return await new Promise((resolve, reject) => { + let settled = false + const timeout = setTimeout(() => { + if (!settled) { + settled = true + reject(new Error('Timeout waiting for kick')) + } + }, timeoutMs) + + bot.once('kicked', (reason) => { + if (settled) return + settled = true + clearTimeout(timeout) + bot.removeAllListeners('end') + this.bot = null + resolve(extractKickReason(reason)) + }) + + bot.once('end', (reason) => { + if (settled) return + settled = true + clearTimeout(timeout) + bot.removeAllListeners('kicked') + this.bot = null + reject(new Error(`Bot disconnected before kick: ${reason}`)) + }) + }) + } + private async sleep (ms: number): Promise { return await new Promise((resolve) => setTimeout(resolve, ms)) } diff --git a/fabric/src/main/java/me/confuser/banmanager/webenhancer/fabric/listeners/EventListener.java b/fabric/src/main/java/me/confuser/banmanager/webenhancer/fabric/listeners/EventListener.java index be416f9..449cf02 100644 --- a/fabric/src/main/java/me/confuser/banmanager/webenhancer/fabric/listeners/EventListener.java +++ b/fabric/src/main/java/me/confuser/banmanager/webenhancer/fabric/listeners/EventListener.java @@ -1,5 +1,6 @@ package me.confuser.banmanager.webenhancer.fabric.listeners; +import me.confuser.banmanager.common.data.PlayerBanData; import me.confuser.banmanager.common.data.PlayerData; import me.confuser.banmanager.common.util.Message; import me.confuser.banmanager.fabric.BanManagerEvents; @@ -15,8 +16,8 @@ public EventListener(WebEnhancerPlugin plugin) { this.plugin = plugin; this.deniedListener = new CommonPlayerDeniedListener(plugin); - // Wire BanManager Fabric events BanManagerEvents.PLAYER_DENIED_EVENT.register(this::onPlayerDenied); + BanManagerEvents.PLAYER_BANNED_EVENT.register(this::onPlayerBanned); BanManagerEvents.PLUGIN_RELOADED_EVENT.register(this::onReload); } @@ -24,6 +25,12 @@ private void onPlayerDenied(PlayerData player, Message message) { deniedListener.handlePin(player, message); } + private void onPlayerBanned(PlayerBanData banData, boolean silent, Message kickMessage) { + if (kickMessage != null) { + deniedListener.handlePin(banData.getPlayer(), kickMessage); + } + } + private void onReload(PlayerData actor) { plugin.setupConfigs(); } diff --git a/sponge-api7/src/main/java/me/confuser/banmanager/webenhancer/sponge/listeners/ReportListener.java b/sponge-api7/src/main/java/me/confuser/banmanager/webenhancer/sponge/listeners/ReportListener.java index 29cb6fb..8272850 100644 --- a/sponge-api7/src/main/java/me/confuser/banmanager/webenhancer/sponge/listeners/ReportListener.java +++ b/sponge-api7/src/main/java/me/confuser/banmanager/webenhancer/sponge/listeners/ReportListener.java @@ -1,10 +1,12 @@ package me.confuser.banmanager.webenhancer.sponge.listeners; import me.confuser.banmanager.common.ormlite.stmt.DeleteBuilder; +import me.confuser.banmanager.sponge.api.events.PlayerBannedEvent; import me.confuser.banmanager.sponge.api.events.PlayerReportDeletedEvent; import me.confuser.banmanager.sponge.api.events.PlayerReportedEvent; import me.confuser.banmanager.sponge.api.events.PlayerDeniedEvent; import me.confuser.banmanager.sponge.api.events.PluginReloadedEvent; +import me.confuser.banmanager.common.util.Message; import me.confuser.banmanager.common.data.PlayerReportData; import me.confuser.banmanager.webenhancer.sponge.SpongePlugin; import me.confuser.banmanager.webenhancer.common.data.LogData; @@ -79,6 +81,17 @@ public void onDeny(final PlayerDeniedEvent event) { listener.handlePin(event.getPlayer(), event.getMessage()); } + @Listener(order = Order.POST) + public void onBanned(PlayerBannedEvent event) { + try { + Message kickMessage = event.getKickMessage(); + if (kickMessage != null) { + listener.handlePin(event.getBan().getPlayer(), kickMessage); + } + } catch (NoSuchMethodError ignored) { + } + } + @Listener public void onReload(PluginReloadedEvent event) { plugin.getPlugin().setupConfigs(); diff --git a/sponge/src/main/java/me/confuser/banmanager/webenhancer/sponge/listeners/ReportListener.java b/sponge/src/main/java/me/confuser/banmanager/webenhancer/sponge/listeners/ReportListener.java index cf048ce..480d215 100644 --- a/sponge/src/main/java/me/confuser/banmanager/webenhancer/sponge/listeners/ReportListener.java +++ b/sponge/src/main/java/me/confuser/banmanager/webenhancer/sponge/listeners/ReportListener.java @@ -1,10 +1,12 @@ package me.confuser.banmanager.webenhancer.sponge.listeners; import me.confuser.banmanager.common.ormlite.stmt.DeleteBuilder; +import me.confuser.banmanager.sponge.api.events.PlayerBannedEvent; import me.confuser.banmanager.sponge.api.events.PlayerReportDeletedEvent; import me.confuser.banmanager.sponge.api.events.PlayerReportedEvent; import me.confuser.banmanager.sponge.api.events.PlayerDeniedEvent; import me.confuser.banmanager.sponge.api.events.PluginReloadedEvent; +import me.confuser.banmanager.common.util.Message; import me.confuser.banmanager.common.data.PlayerReportData; import me.confuser.banmanager.webenhancer.sponge.SpongePlugin; import me.confuser.banmanager.webenhancer.common.data.LogData; @@ -77,6 +79,17 @@ public void onDeny(final PlayerDeniedEvent event) { listener.handlePin(event.getPlayer(), event.getMessage()); } + @Listener(order = Order.POST) + public void onBanned(PlayerBannedEvent event) { + try { + Message kickMessage = event.getKickMessage(); + if (kickMessage != null) { + listener.handlePin(event.getBan().getPlayer(), kickMessage); + } + } catch (NoSuchMethodError ignored) { + } + } + @Listener public void onReload(PluginReloadedEvent event) { plugin.getPlugin().setupConfigs(); diff --git a/velocity/src/main/java/me/confuser/banmanager/webenhancer/velocity/listener/BanListener.java b/velocity/src/main/java/me/confuser/banmanager/webenhancer/velocity/listener/BanListener.java index 2eb2bf8..29e1548 100644 --- a/velocity/src/main/java/me/confuser/banmanager/webenhancer/velocity/listener/BanListener.java +++ b/velocity/src/main/java/me/confuser/banmanager/webenhancer/velocity/listener/BanListener.java @@ -2,7 +2,9 @@ import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.proxy.ProxyReloadEvent; +import me.confuser.banmanager.common.util.Message; import me.confuser.banmanager.velocity.Listener; +import me.confuser.banmanager.velocity.api.events.PlayerBannedEvent; import me.confuser.banmanager.velocity.api.events.PlayerDeniedEvent; import me.confuser.banmanager.webenhancer.common.listeners.CommonPlayerDeniedListener; import me.confuser.banmanager.webenhancer.velocity.VelocityPlugin; @@ -21,6 +23,17 @@ public void onDeny(PlayerDeniedEvent event) { listener.handlePin(event.getPlayer(), event.getMessage()); } + @Subscribe + public void onBanned(PlayerBannedEvent event) { + try { + Message kickMessage = event.getKickMessage(); + if (kickMessage != null) { + listener.handlePin(event.getBan().getPlayer(), kickMessage); + } + } catch (NoSuchMethodError ignored) { + } + } + @Subscribe public void onReload(ProxyReloadEvent event) { velocityPlugin.getPlugin().setupConfigs(); From 502aa6ad9820290d6f99dda77dd088130c825238 Mon Sep 17 00:00:00 2001 From: James Mortemore Date: Fri, 3 Apr 2026 22:52:14 +0100 Subject: [PATCH 02/10] fix: add [pin] placeholder to ban/tempban kick message templates The e2e message configs only had [pin] in the disallowed (reconnect denial) templates but not in the kick (online player) templates, causing the Online Kick Pin tests to fail. --- e2e/platforms/bukkit/configs/banmanager/messages.yml | 4 ++-- e2e/platforms/bungee/configs/banmanager/messages.yml | 4 ++-- e2e/platforms/fabric/configs/banmanager/messages.yml | 4 ++-- e2e/platforms/sponge/configs/banmanager/messages.yml | 4 ++-- e2e/platforms/sponge7/configs/banmanager/messages.yml | 4 ++-- e2e/platforms/velocity/configs/banmanager/messages.yml | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/e2e/platforms/bukkit/configs/banmanager/messages.yml b/e2e/platforms/bukkit/configs/banmanager/messages.yml index fdf0aab..459dab6 100644 --- a/e2e/platforms/bukkit/configs/banmanager/messages.yml +++ b/e2e/platforms/bukkit/configs/banmanager/messages.yml @@ -136,7 +136,7 @@ messages: ban: player: disallowed: '&6You have been banned from this server for &4[reason]&6. Your appeal pin is [pin]' - kick: '&6You have been banned permanently for &4[reason]' + kick: '&6You have been banned permanently for &4[reason]&6. Your appeal pin is [pin]' dateTimeFormat: 'yyyy-MM-dd HH:mm:ss' notify: '&6[player] has been permanently banned by [actor] for &4[reason]' error: @@ -149,7 +149,7 @@ messages: tempban: player: disallowed: '&6You have been temporarily banned from this server for &4[reason] \n&6It expires in [expires]. Your appeal pin is [pin]' - kick: '&6You have been temporarily banned for &4[reason]' + kick: '&6You have been temporarily banned for &4[reason]&6. Your appeal pin is [pin]' dateTimeFormat: 'yyyy-MM-dd HH:mm:ss' notify: '&6[player] has been temporarily banned for [expires] by [actor] for &4[reason]' diff --git a/e2e/platforms/bungee/configs/banmanager/messages.yml b/e2e/platforms/bungee/configs/banmanager/messages.yml index 118dd99..5bacf5c 100644 --- a/e2e/platforms/bungee/configs/banmanager/messages.yml +++ b/e2e/platforms/bungee/configs/banmanager/messages.yml @@ -143,7 +143,7 @@ messages: player: disallowed: '&6You have been banned from this server for &4[reason]&6. Your appeal pin is [pin]' - kick: '&6You have been banned permanently for &4[reason]' + kick: '&6You have been banned permanently for &4[reason]&6. Your appeal pin is [pin]' dateTimeFormat: yyyy-MM-dd HH:mm:ss notify: '&6[player] has been permanently banned by [actor] for &4[reason]' error: @@ -155,7 +155,7 @@ messages: player: disallowed: '&6You have been temporarily banned from this server for &4[reason] \n&6It expires in [expires]. Your appeal pin is [pin]' - kick: '&6You have been temporarily banned for &4[reason]' + kick: '&6You have been temporarily banned for &4[reason]&6. Your appeal pin is [pin]' dateTimeFormat: yyyy-MM-dd HH:mm:ss notify: '&6[player] has been temporarily banned for [expires] by [actor] for &4[reason]' tempbanall: diff --git a/e2e/platforms/fabric/configs/banmanager/messages.yml b/e2e/platforms/fabric/configs/banmanager/messages.yml index fb8d205..8a833ce 100644 --- a/e2e/platforms/fabric/configs/banmanager/messages.yml +++ b/e2e/platforms/fabric/configs/banmanager/messages.yml @@ -142,7 +142,7 @@ messages: ban: player: disallowed: '&6You have been banned from this server for &4[reason]&6. Your appeal pin is [pin]' - kick: '&6You have been banned permanently for &4[reason]' + kick: '&6You have been banned permanently for &4[reason]&6. Your appeal pin is [pin]' dateTimeFormat: 'yyyy-MM-dd HH:mm:ss' notify: '&6[player] has been permanently banned by [actor] for &4[reason]' error: @@ -155,7 +155,7 @@ messages: tempban: player: disallowed: '&6You have been temporarily banned from this server for &4[reason] \n&6It expires in [expires]. Your appeal pin is [pin]' - kick: '&6You have been temporarily banned for &4[reason]' + kick: '&6You have been temporarily banned for &4[reason]&6. Your appeal pin is [pin]' dateTimeFormat: 'yyyy-MM-dd HH:mm:ss' notify: '&6[player] has been temporarily banned for [expires] by [actor] for &4[reason]' diff --git a/e2e/platforms/sponge/configs/banmanager/messages.yml b/e2e/platforms/sponge/configs/banmanager/messages.yml index fdf0aab..459dab6 100644 --- a/e2e/platforms/sponge/configs/banmanager/messages.yml +++ b/e2e/platforms/sponge/configs/banmanager/messages.yml @@ -136,7 +136,7 @@ messages: ban: player: disallowed: '&6You have been banned from this server for &4[reason]&6. Your appeal pin is [pin]' - kick: '&6You have been banned permanently for &4[reason]' + kick: '&6You have been banned permanently for &4[reason]&6. Your appeal pin is [pin]' dateTimeFormat: 'yyyy-MM-dd HH:mm:ss' notify: '&6[player] has been permanently banned by [actor] for &4[reason]' error: @@ -149,7 +149,7 @@ messages: tempban: player: disallowed: '&6You have been temporarily banned from this server for &4[reason] \n&6It expires in [expires]. Your appeal pin is [pin]' - kick: '&6You have been temporarily banned for &4[reason]' + kick: '&6You have been temporarily banned for &4[reason]&6. Your appeal pin is [pin]' dateTimeFormat: 'yyyy-MM-dd HH:mm:ss' notify: '&6[player] has been temporarily banned for [expires] by [actor] for &4[reason]' diff --git a/e2e/platforms/sponge7/configs/banmanager/messages.yml b/e2e/platforms/sponge7/configs/banmanager/messages.yml index fdf0aab..459dab6 100644 --- a/e2e/platforms/sponge7/configs/banmanager/messages.yml +++ b/e2e/platforms/sponge7/configs/banmanager/messages.yml @@ -136,7 +136,7 @@ messages: ban: player: disallowed: '&6You have been banned from this server for &4[reason]&6. Your appeal pin is [pin]' - kick: '&6You have been banned permanently for &4[reason]' + kick: '&6You have been banned permanently for &4[reason]&6. Your appeal pin is [pin]' dateTimeFormat: 'yyyy-MM-dd HH:mm:ss' notify: '&6[player] has been permanently banned by [actor] for &4[reason]' error: @@ -149,7 +149,7 @@ messages: tempban: player: disallowed: '&6You have been temporarily banned from this server for &4[reason] \n&6It expires in [expires]. Your appeal pin is [pin]' - kick: '&6You have been temporarily banned for &4[reason]' + kick: '&6You have been temporarily banned for &4[reason]&6. Your appeal pin is [pin]' dateTimeFormat: 'yyyy-MM-dd HH:mm:ss' notify: '&6[player] has been temporarily banned for [expires] by [actor] for &4[reason]' diff --git a/e2e/platforms/velocity/configs/banmanager/messages.yml b/e2e/platforms/velocity/configs/banmanager/messages.yml index 118dd99..5bacf5c 100644 --- a/e2e/platforms/velocity/configs/banmanager/messages.yml +++ b/e2e/platforms/velocity/configs/banmanager/messages.yml @@ -143,7 +143,7 @@ messages: player: disallowed: '&6You have been banned from this server for &4[reason]&6. Your appeal pin is [pin]' - kick: '&6You have been banned permanently for &4[reason]' + kick: '&6You have been banned permanently for &4[reason]&6. Your appeal pin is [pin]' dateTimeFormat: yyyy-MM-dd HH:mm:ss notify: '&6[player] has been permanently banned by [actor] for &4[reason]' error: @@ -155,7 +155,7 @@ messages: player: disallowed: '&6You have been temporarily banned from this server for &4[reason] \n&6It expires in [expires]. Your appeal pin is [pin]' - kick: '&6You have been temporarily banned for &4[reason]' + kick: '&6You have been temporarily banned for &4[reason]&6. Your appeal pin is [pin]' dateTimeFormat: yyyy-MM-dd HH:mm:ss notify: '&6[player] has been temporarily banned for [expires] by [actor] for &4[reason]' tempbanall: From 0dc588932ee45c4911d6785c6fe953e1d2933f47 Mon Sep 17 00:00:00 2001 From: James Mortemore Date: Sat, 4 Apr 2026 00:02:59 +0100 Subject: [PATCH 03/10] fix: improve e2e test resilience for kick and denial tests - Handle NBT compound chat components in kick reasons (Paper 1.20.5+) - Add delay in waitForKick end handler to avoid socketClosed race - Increase denial retry attempts and sleeps for async ban persistence - Disconnect bot before unban in afterEach to avoid stale kick on reconnect --- e2e/tests/src/denied-pin.test.ts | 11 +++--- e2e/tests/src/helpers/bot.ts | 57 ++++++++++++++++++++++++++++---- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/e2e/tests/src/denied-pin.test.ts b/e2e/tests/src/denied-pin.test.ts index 10887b7..150a535 100644 --- a/e2e/tests/src/denied-pin.test.ts +++ b/e2e/tests/src/denied-pin.test.ts @@ -13,7 +13,7 @@ describe('Denied Pin Placeholder', () => { let lastError: Error | null = null // Ban/tempban enforcement can be async across worker threads; retry denied connect checks. - for (let attempt = 1; attempt <= 3; attempt++) { + for (let attempt = 1; attempt <= 5; attempt++) { try { bannedBot = await createBot(BANNED_USERNAME) await bannedBot.disconnect() @@ -27,7 +27,7 @@ describe('Denied Pin Placeholder', () => { lastError = error instanceof Error ? error : new Error(String(error)) } - await sleep(1000) + await sleep(2000) } throw lastError ?? new Error('Expected denied connection but did not receive a denial kick') @@ -64,7 +64,7 @@ describe('Denied Pin Placeholder', () => { test('[pin] placeholder in tempban message is replaced with actual pin', async () => { const tempbanResponse = await sendCommand(`bmtempban ${BANNED_USERNAME} 1h Testing tempban pin placeholder`) console.log(`Tempban response: ${tempbanResponse}`) - await sleep(2000) + await sleep(3000) const errorMessage = await expectDeniedConnection() console.log(`Player was denied (tempban) as expected: ${errorMessage}`) @@ -89,11 +89,12 @@ describe('Online Kick Pin Placeholder', () => { let bot: TestBot | null = null afterEach(async () => { + await bot?.disconnect() + bot = null try { await sendCommand(`bmunban ${KICK_USERNAME}`) } catch (e) {} - await bot?.disconnect() - bot = null + await sleep(2000) }) test('[pin] placeholder in kick message is replaced when banning an online player', async () => { diff --git a/e2e/tests/src/helpers/bot.ts b/e2e/tests/src/helpers/bot.ts index bc13589..442b5b0 100644 --- a/e2e/tests/src/helpers/bot.ts +++ b/e2e/tests/src/helpers/bot.ts @@ -6,7 +6,40 @@ const SERVER_PORT = parseInt(process.env.SERVER_PORT ?? '25565', 10) const MC_VERSION = process.env.MC_VERSION ?? undefined /** - * Extract text from a kick reason (string on Bukkit, ChatMessage object on Fabric) + * Extract text from an NBT compound chat component (Paper 1.20.5+ sends kick + * reasons as PrismarineNBT compounds instead of JSON chat components). + */ +function extractNbtText (nbt: Record): string { + const value = nbt.value as Record + let result = '' + + const textField = value.text as Record | undefined + if (textField?.type === 'string' && typeof textField.value === 'string') { + result += textField.value + } + + const extra = value.extra as Record | undefined + if (extra?.type === 'list') { + const listVal = extra.value as Record + const items = listVal?.value + if (Array.isArray(items)) { + for (const item of items) { + if (typeof item === 'object' && item != null) { + const t = (item as Record).text as Record | undefined + if (t?.type === 'string' && typeof t.value === 'string') { + result += t.value + } + } + } + } + } + + return result +} + +/** + * Extract text from a kick reason (string on Bukkit, ChatMessage object on + * Fabric, or NBT compound on newer Paper). */ function extractKickReason (reason: unknown): string { if (typeof reason === 'string') { @@ -14,6 +47,11 @@ function extractKickReason (reason: unknown): string { } if (reason != null && typeof reason === 'object') { const reasonObj = reason as Record + + if (reasonObj.type === 'compound' && reasonObj.value != null) { + return extractNbtText(reasonObj) + } + if (typeof reasonObj.toString === 'function') { const result = reasonObj.toString() if (result !== '[object Object]') { @@ -315,12 +353,17 @@ export class TestBot { }) bot.once('end', (reason) => { - if (settled) return - settled = true - clearTimeout(timeout) - bot.removeAllListeners('kicked') - this.bot = null - reject(new Error(`Bot disconnected before kick: ${reason}`)) + // Delay before rejecting — mineflayer can fire 'end' (socketClosed) before + // 'kicked' when the server closes the TCP connection immediately after + // sending the disconnect packet. Give 'kicked' a chance to arrive first. + setTimeout(() => { + if (settled) return + settled = true + clearTimeout(timeout) + bot.removeAllListeners('kicked') + this.bot = null + reject(new Error(`Bot disconnected before kick: ${reason}`)) + }, 500) }) }) } From a589593aaccd833008f8d0ab36d1b71f7f254cac Mon Sep 17 00:00:00 2001 From: James Mortemore Date: Sat, 4 Apr 2026 08:51:52 +0100 Subject: [PATCH 04/10] fix: prevent stale SNAPSHOT dependencies in CI - Add --refresh-dependencies to build step to force fresh SNAPSHOT resolution - Use run_id in Loom cache key to prevent serving stale remapped dependency JARs --- .github/workflows/build.yml | 4 ++-- .github/workflows/e2e.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a127f2a..f889f4d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,13 +47,13 @@ jobs: with: path: | .gradle/loom-cache - key: ${{ runner.os }}-loom-${{ hashFiles('**/libs.versions.*', '**/*.gradle*', '**/gradle-wrapper.properties') }} + key: ${{ runner.os }}-loom-${{ hashFiles('**/libs.versions.*', '**/*.gradle*', '**/gradle-wrapper.properties') }}-${{ github.run_id }} restore-keys: ${{ runner.os }}-loom- - name: Execute Gradle build env: STORAGE_TYPE: ${{ matrix.storageType }} - run: ./gradlew build --build-cache --info + run: ./gradlew build --build-cache --refresh-dependencies --info - name: Build all Fabric versions run: ./gradlew :fabric:1.20.1:remapJar :fabric:1.21.1:remapJar :fabric:1.21.4:remapJar :fabric:1.21.11:remapJar --build-cache diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 242ca5c..da3361e 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -132,7 +132,7 @@ jobs: with: path: | .gradle/loom-cache - key: ${{ runner.os }}-loom-${{ hashFiles('**/libs.versions.*', '**/*.gradle*', '**/gradle-wrapper.properties') }} + key: ${{ runner.os }}-loom-${{ hashFiles('**/libs.versions.*', '**/*.gradle*', '**/gradle-wrapper.properties') }}-${{ github.run_id }} restore-keys: ${{ runner.os }}-loom- # Docker Buildx for better caching From a5fc15213044b0054cea7ec1614abe4ac1ec4df9 Mon Sep 17 00:00:00 2001 From: James Mortemore Date: Sat, 4 Apr 2026 10:55:52 +0100 Subject: [PATCH 05/10] fix: compile against BanManager from source and clean up bot on kick - Checkout BanManager and publishToMavenLocal in both CI workflows so WebEnhancer always compiles against the latest API (Fabric Stonecutter artifacts are not reliably published to Maven Central snapshots) - Clean up mineflayer bot (removeAllListeners + end) on kick/error during connect() and waitForKick() to prevent physics timers running after Jest teardown --- .github/workflows/build.yml | 14 ++++++++++++-- .github/workflows/e2e.yml | 6 +++++- e2e/tests/src/helpers/bot.ts | 16 ++++++++++++++-- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f889f4d..1df5e2f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,6 +26,12 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + repository: BanManagement/BanManager + path: BanManager + ref: master + - name: Set up JDK 17 uses: actions/setup-java@v4 with: @@ -47,13 +53,17 @@ jobs: with: path: | .gradle/loom-cache - key: ${{ runner.os }}-loom-${{ hashFiles('**/libs.versions.*', '**/*.gradle*', '**/gradle-wrapper.properties') }}-${{ github.run_id }} + key: ${{ runner.os }}-loom-${{ hashFiles('**/libs.versions.*', '**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: ${{ runner.os }}-loom- + - name: Publish BanManager to Maven Local + working-directory: BanManager + run: ./gradlew publishToMavenLocal --build-cache + - name: Execute Gradle build env: STORAGE_TYPE: ${{ matrix.storageType }} - run: ./gradlew build --build-cache --refresh-dependencies --info + run: ./gradlew build --build-cache --info - name: Build all Fabric versions run: ./gradlew :fabric:1.20.1:remapJar :fabric:1.21.1:remapJar :fabric:1.21.4:remapJar :fabric:1.21.11:remapJar --build-cache diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index da3361e..a3b5bd3 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -132,9 +132,13 @@ jobs: with: path: | .gradle/loom-cache - key: ${{ runner.os }}-loom-${{ hashFiles('**/libs.versions.*', '**/*.gradle*', '**/gradle-wrapper.properties') }}-${{ github.run_id }} + key: ${{ runner.os }}-loom-${{ hashFiles('**/libs.versions.*', '**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: ${{ runner.os }}-loom- + - name: Publish BanManager to Maven Local + working-directory: BanManager + run: ./gradlew publishToMavenLocal --build-cache + # Docker Buildx for better caching - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/e2e/tests/src/helpers/bot.ts b/e2e/tests/src/helpers/bot.ts index 442b5b0..eaa3b06 100644 --- a/e2e/tests/src/helpers/bot.ts +++ b/e2e/tests/src/helpers/bot.ts @@ -115,7 +115,16 @@ export class TestBot { version: MC_VERSION }) + const cleanup = (): void => { + if (this.bot != null) { + this.bot.removeAllListeners() + this.bot.end() + this.bot = null + } + } + const timeout = setTimeout(() => { + cleanup() reject(new Error('Bot connection timeout')) }, 30000) @@ -127,6 +136,7 @@ export class TestBot { this.bot.once('error', (err) => { clearTimeout(timeout) + cleanup() reject(err) }) @@ -134,6 +144,7 @@ export class TestBot { clearTimeout(timeout) const reasonText = extractKickReason(reason) console.log(`Bot ${this._username} was kicked: ${reasonText}`) + cleanup() reject(new Error(`Bot ${this._username} was kicked: ${reasonText}`)) }) @@ -347,7 +358,8 @@ export class TestBot { if (settled) return settled = true clearTimeout(timeout) - bot.removeAllListeners('end') + bot.removeAllListeners() + bot.end() this.bot = null resolve(extractKickReason(reason)) }) @@ -360,7 +372,7 @@ export class TestBot { if (settled) return settled = true clearTimeout(timeout) - bot.removeAllListeners('kicked') + bot.removeAllListeners() this.bot = null reject(new Error(`Bot disconnected before kick: ${reason}`)) }, 500) From 497cd28fa226d422e80aaf5cbaa1fbcf6df0c3e1 Mon Sep 17 00:00:00 2001 From: James Mortemore Date: Sat, 4 Apr 2026 11:24:26 +0100 Subject: [PATCH 06/10] fix: force fresh BanManager dependency resolution and await bot cleanup - Clear BanManager artifacts from Gradle and Loom caches before build so publishToMavenLocal takes priority over stale cached SNAPSHOTs - Add --refresh-dependencies to Gradle build commands to bypass dependency resolution cache entirely - Wait for mineflayer 'end' event before resolving waitForKick() and connect() cleanup, so the physics timer is cleared before Jest teardown begins (prevents prismarine-physics ReferenceError) - Stop calling removeAllListeners() before bot.end() which stripped mineflayer's internal cleanup handlers --- .github/workflows/build.yml | 9 ++++++-- .github/workflows/e2e.yml | 7 +++++- e2e/tests/src/helpers/bot.ts | 45 +++++++++++++++++++++++++----------- 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1df5e2f..6e6b933 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,10 +60,15 @@ jobs: working-directory: BanManager run: ./gradlew publishToMavenLocal --build-cache + - name: Clear stale BanManager dependency caches + run: | + find ~/.gradle/caches -path "*/me.confuser.banmanager*" -exec rm -rf {} + 2>/dev/null || true + find .gradle/loom-cache -path "*/BanManager*" -exec rm -rf {} + 2>/dev/null || true + - name: Execute Gradle build env: STORAGE_TYPE: ${{ matrix.storageType }} - run: ./gradlew build --build-cache --info + run: ./gradlew build --build-cache --refresh-dependencies --info - name: Build all Fabric versions - run: ./gradlew :fabric:1.20.1:remapJar :fabric:1.21.1:remapJar :fabric:1.21.4:remapJar :fabric:1.21.11:remapJar --build-cache + run: ./gradlew :fabric:1.20.1:remapJar :fabric:1.21.1:remapJar :fabric:1.21.4:remapJar :fabric:1.21.11:remapJar --build-cache --refresh-dependencies diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index a3b5bd3..768eca2 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -139,6 +139,11 @@ jobs: working-directory: BanManager run: ./gradlew publishToMavenLocal --build-cache + - name: Clear stale BanManager dependency caches + run: | + find ~/.gradle/caches -path "*/me.confuser.banmanager*" -exec rm -rf {} + 2>/dev/null || true + find .gradle/loom-cache -path "*/BanManager*" -exec rm -rf {} + 2>/dev/null || true + # Docker Buildx for better caching - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -175,7 +180,7 @@ jobs: run: ./gradlew ${{ matrix.build_task }} --build-cache - name: Run E2E tests - run: ./gradlew :BanManagerWebEnhancerE2E:${{ matrix.task }} --build-cache -PbanManagerPath=BanManager + run: ./gradlew :BanManagerWebEnhancerE2E:${{ matrix.task }} --build-cache --refresh-dependencies -PbanManagerPath=BanManager timeout-minutes: 15 env: MC_VERSION: ${{ matrix.mc_version }} diff --git a/e2e/tests/src/helpers/bot.ts b/e2e/tests/src/helpers/bot.ts index eaa3b06..6155264 100644 --- a/e2e/tests/src/helpers/bot.ts +++ b/e2e/tests/src/helpers/bot.ts @@ -115,17 +115,25 @@ export class TestBot { version: MC_VERSION }) - const cleanup = (): void => { + const cleanup = (callback: () => void): void => { if (this.bot != null) { - this.bot.removeAllListeners() - this.bot.end() + const b = this.bot this.bot = null + + const fallback = setTimeout(() => callback(), 1500) + + b.once('end', () => { + clearTimeout(fallback) + callback() + }) + b.end() + } else { + callback() } } const timeout = setTimeout(() => { - cleanup() - reject(new Error('Bot connection timeout')) + cleanup(() => reject(new Error('Bot connection timeout'))) }, 30000) this.bot.once('spawn', () => { @@ -136,16 +144,14 @@ export class TestBot { this.bot.once('error', (err) => { clearTimeout(timeout) - cleanup() - reject(err) + cleanup(() => reject(err)) }) this.bot.once('kicked', (reason) => { clearTimeout(timeout) const reasonText = extractKickReason(reason) console.log(`Bot ${this._username} was kicked: ${reasonText}`) - cleanup() - reject(new Error(`Bot ${this._username} was kicked: ${reasonText}`)) + cleanup(() => reject(new Error(`Bot ${this._username} was kicked: ${reasonText}`))) }) this.bot.on('chat', (username, message) => { @@ -358,10 +364,24 @@ export class TestBot { if (settled) return settled = true clearTimeout(timeout) - bot.removeAllListeners() + const kickReason = extractKickReason(reason) + + // Wait for the 'end' event so mineflayer's internal cleanup (physics + // timer clearInterval) runs before we resolve. Resolving immediately + // lets the test finish and Jest tear down while the physics tick is + // still running, causing a ReferenceError. + const endTimeout = setTimeout(() => { + this.bot = null + resolve(kickReason) + }, 1500) + + bot.once('end', () => { + clearTimeout(endTimeout) + this.bot = null + resolve(kickReason) + }) + bot.end() - this.bot = null - resolve(extractKickReason(reason)) }) bot.once('end', (reason) => { @@ -372,7 +392,6 @@ export class TestBot { if (settled) return settled = true clearTimeout(timeout) - bot.removeAllListeners() this.bot = null reject(new Error(`Bot disconnected before kick: ${reason}`)) }, 500) From baf1e5f9100beeb7735ec0934fe9231189563e53 Mon Sep 17 00:00:00 2001 From: James Mortemore Date: Sat, 4 Apr 2026 11:39:38 +0100 Subject: [PATCH 07/10] chore: bump version to 7.11.0-SNAPSHOT to match BanManager --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 18c8d47..2017cee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=me.confuser.banmanager.webenhancer -version=7.7.0-SNAPSHOT +version=7.11.0-SNAPSHOT org.gradle.parallel=true org.gradle.jvmargs=-Xmx2G -XX:+UseG1GC -XX:MaxGCPauseMillis=200 From 65d3e3ce98046b96773334ed4a4daa2969420e48 Mon Sep 17 00:00:00 2001 From: James Mortemore Date: Sat, 4 Apr 2026 11:55:06 +0100 Subject: [PATCH 08/10] fix: remove --refresh-dependencies from Fabric remapJar step to avoid OOM Dependencies are already freshly resolved by the preceding build step. The redundant --refresh-dependencies on the memory-intensive Loom remapping step caused OutOfMemoryError. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6e6b933..7c5549a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -71,4 +71,4 @@ jobs: run: ./gradlew build --build-cache --refresh-dependencies --info - name: Build all Fabric versions - run: ./gradlew :fabric:1.20.1:remapJar :fabric:1.21.1:remapJar :fabric:1.21.4:remapJar :fabric:1.21.11:remapJar --build-cache --refresh-dependencies + run: ./gradlew :fabric:1.20.1:remapJar :fabric:1.21.1:remapJar :fabric:1.21.4:remapJar :fabric:1.21.11:remapJar --build-cache From c036985bb1083582658f929e44ff046316858456 Mon Sep 17 00:00:00 2001 From: James Mortemore Date: Sat, 4 Apr 2026 12:22:36 +0100 Subject: [PATCH 09/10] fix: improve denied-pin test cleanup and timing for Fabric - Clear both permanent and temp bans in afterEach (bmunban + bmuntempban) - Disconnect bot before unban to avoid stale state - Add 2s sleep after cleanup for async processing to settle - Increase sleep after bmtempban from 3s to 5s for Fabric enforcement lag --- e2e/tests/src/denied-pin.test.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/e2e/tests/src/denied-pin.test.ts b/e2e/tests/src/denied-pin.test.ts index 150a535..e85e648 100644 --- a/e2e/tests/src/denied-pin.test.ts +++ b/e2e/tests/src/denied-pin.test.ts @@ -34,11 +34,11 @@ describe('Denied Pin Placeholder', () => { } afterEach(async () => { - try { - await sendCommand(`bmunban ${BANNED_USERNAME}`) - } catch (e) {} await bannedBot?.disconnect() bannedBot = null + try { await sendCommand(`bmunban ${BANNED_USERNAME}`) } catch (e) {} + try { await sendCommand(`bmuntempban ${BANNED_USERNAME}`) } catch (e) {} + await sleep(2000) }) test('[pin] placeholder in ban message is replaced with actual pin', async () => { @@ -64,7 +64,7 @@ describe('Denied Pin Placeholder', () => { test('[pin] placeholder in tempban message is replaced with actual pin', async () => { const tempbanResponse = await sendCommand(`bmtempban ${BANNED_USERNAME} 1h Testing tempban pin placeholder`) console.log(`Tempban response: ${tempbanResponse}`) - await sleep(3000) + await sleep(5000) const errorMessage = await expectDeniedConnection() console.log(`Player was denied (tempban) as expected: ${errorMessage}`) @@ -91,9 +91,8 @@ describe('Online Kick Pin Placeholder', () => { afterEach(async () => { await bot?.disconnect() bot = null - try { - await sendCommand(`bmunban ${KICK_USERNAME}`) - } catch (e) {} + try { await sendCommand(`bmunban ${KICK_USERNAME}`) } catch (e) {} + try { await sendCommand(`bmuntempban ${KICK_USERNAME}`) } catch (e) {} await sleep(2000) }) From ec31fe72340c237883e2dafddbc3f268ef0c7ba3 Mon Sep 17 00:00:00 2001 From: James Mortemore Date: Sat, 4 Apr 2026 12:35:19 +0100 Subject: [PATCH 10/10] fix: use short connect timeout in denied connection checks On Fabric 1.21.4, tempbanned players sometimes enter a limbo state (connected but neither spawned nor kicked) consuming the full 30s connect timeout per attempt. Use a 10s timeout with 8 retries instead, and increase the test timeout to 120s for headroom. --- e2e/tests/src/denied-pin.test.ts | 16 ++++++++-------- e2e/tests/src/helpers/bot.ts | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/e2e/tests/src/denied-pin.test.ts b/e2e/tests/src/denied-pin.test.ts index e85e648..13aa988 100644 --- a/e2e/tests/src/denied-pin.test.ts +++ b/e2e/tests/src/denied-pin.test.ts @@ -5,19 +5,20 @@ afterAll(async () => { }) describe('Denied Pin Placeholder', () => { - let bannedBot: TestBot | null = null const BANNED_USERNAME = 'DeniedPinPlayer' - const TEST_TIMEOUT_MS = 60000 + const TEST_TIMEOUT_MS = 120000 const expectDeniedConnection = async (): Promise => { let lastError: Error | null = null // Ban/tempban enforcement can be async across worker threads; retry denied connect checks. - for (let attempt = 1; attempt <= 5; attempt++) { + // Use a short connect timeout (10s) so limbo connections (neither kicked nor spawned) + // don't burn the full 30s default, leaving room for more retries. + for (let attempt = 1; attempt <= 8; attempt++) { + const bot = new TestBot(BANNED_USERNAME) try { - bannedBot = await createBot(BANNED_USERNAME) - await bannedBot.disconnect() - bannedBot = null + await bot.connect(10000) + await bot.disconnect() lastError = new Error(`Attempt ${attempt}: player connected while expected to be denied`) } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error) @@ -25,6 +26,7 @@ describe('Denied Pin Placeholder', () => { return errorMessage } lastError = error instanceof Error ? error : new Error(String(error)) + try { await bot.disconnect() } catch (e) {} } await sleep(2000) @@ -34,8 +36,6 @@ describe('Denied Pin Placeholder', () => { } afterEach(async () => { - await bannedBot?.disconnect() - bannedBot = null try { await sendCommand(`bmunban ${BANNED_USERNAME}`) } catch (e) {} try { await sendCommand(`bmuntempban ${BANNED_USERNAME}`) } catch (e) {} await sleep(2000) diff --git a/e2e/tests/src/helpers/bot.ts b/e2e/tests/src/helpers/bot.ts index 6155264..539fbec 100644 --- a/e2e/tests/src/helpers/bot.ts +++ b/e2e/tests/src/helpers/bot.ts @@ -102,7 +102,7 @@ export class TestBot { return this._username } - async connect (): Promise { + async connect (connectTimeoutMs: number = 30000): Promise { return await new Promise((resolve, reject) => { console.log(`Connecting bot ${this._username} to ${SERVER_HOST}:${SERVER_PORT}`) @@ -134,7 +134,7 @@ export class TestBot { const timeout = setTimeout(() => { cleanup(() => reject(new Error('Bot connection timeout'))) - }, 30000) + }, connectTimeoutMs) this.bot.once('spawn', () => { clearTimeout(timeout)