diff --git a/.github/workflows/test-gradle.yml b/.github/workflows/test-gradle.yml index 5d0a605..a25ee8f 100644 --- a/.github/workflows/test-gradle.yml +++ b/.github/workflows/test-gradle.yml @@ -19,6 +19,26 @@ jobs: with: gradle-version: '9.4.1' - name: Build + run: gradle build -x test --no-daemon + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + build-and-test: + runs-on: ubuntu-latest + env: + JAVA_TOOL_OPTIONS: -Djavax.net.ssl.trustStoreType=JKS -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStorePassword=changeit + + steps: + - uses: actions/checkout@v5 + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + java-version: '21' + distribution: 'zulu' + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: '9.4.1' + - name: Build and Test run: gradle build --no-daemon env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Bukkit/Plugin/build.gradle b/Bukkit/Plugin/build.gradle index c4f4cba..f4f9960 100644 --- a/Bukkit/Plugin/build.gradle +++ b/Bukkit/Plugin/build.gradle @@ -4,9 +4,14 @@ plugins { dependencies { compileOnly 'org.spigotmc:spigot-api:1.20.2-R0.1-SNAPSHOT' - compileOnly project(path: ':Common:Source', configuration: 'shadow') - compileOnly project(':Common:loader-utils') + implementation project(':Common:Source') + implementation project(':Common:loader-utils') implementation 'org.bstats:bstats-bukkit:2.2.1' + testImplementation 'com.github.seeseemelk:MockBukkit-v1.20:3.84.0' + testImplementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' + testImplementation 'org.mockito:mockito-core:5.11.0' + testImplementation 'org.mockito:mockito-subclass:5.11.0' + testImplementation 'org.mockito:mockito-junit-jupiter:5.11.0' } shadowJar { @@ -15,4 +20,9 @@ shadowJar { relocate 'org.yaml.snakeyaml', 'dev.brighten.antivpn.shaded.org.yaml.snakeyaml' } +test { + useJUnitPlatform() + systemProperty 'mockito.mockmaker', 'subclass' +} + tasks.build.dependsOn shadowJar diff --git a/Bukkit/Plugin/src/test/java/dev/brighten/antivpn/bukkit/BukkitListenerTest.java b/Bukkit/Plugin/src/test/java/dev/brighten/antivpn/bukkit/BukkitListenerTest.java new file mode 100644 index 0000000..e64acef --- /dev/null +++ b/Bukkit/Plugin/src/test/java/dev/brighten/antivpn/bukkit/BukkitListenerTest.java @@ -0,0 +1,111 @@ +package dev.brighten.antivpn.bukkit; + +import be.seeseemelk.mockbukkit.MockBukkit; +import be.seeseemelk.mockbukkit.ServerMock; +import be.seeseemelk.mockbukkit.entity.PlayerMock; +import dev.brighten.antivpn.AntiVPN; +import dev.brighten.antivpn.api.PlayerExecutor; +import dev.brighten.antivpn.api.VPNConfig; +import dev.brighten.antivpn.api.VPNExecutor; +import dev.brighten.antivpn.message.MessageHandler; +import dev.brighten.antivpn.message.VpnString; +import dev.brighten.antivpn.web.objects.VPNResponse; +import org.bukkit.event.player.PlayerLoginEvent; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +public class BukkitListenerTest { + + private ServerMock server; + private BukkitListener listener; + private VPNExecutor vpnExecutor; + + @BeforeEach + public void setUp() throws Exception { + server = MockBukkit.mock(); + + AntiVPN antiVPN = mock(AntiVPN.class); + VPNConfig config = mock(VPNConfig.class); + PlayerExecutor playerExecutor = mock(PlayerExecutor.class); + vpnExecutor = mock(VPNExecutor.class); + MessageHandler messageHandler = mock(MessageHandler.class); + + when(antiVPN.getVpnConfig()).thenReturn(config); + when(antiVPN.getPlayerExecutor()).thenReturn(playerExecutor); + when(antiVPN.getExecutor()).thenReturn(vpnExecutor); + when(antiVPN.getMessageHandler()).thenReturn(messageHandler); + + when(playerExecutor.getPlayer(any(UUID.class))).thenReturn(Optional.empty()); + when(config.getPrefixWhitelists()).thenReturn(java.util.Collections.emptyList()); + when(config.getCountryList()).thenReturn(java.util.Collections.emptyList()); + when(config.isKickPlayers()).thenReturn(true); + when(config.getKickMessage()).thenReturn("Blocked!"); + + VpnString mockVpnString = mock(VpnString.class); + when(mockVpnString.getFormattedMessage(any())).thenReturn("Blocked!"); + when(messageHandler.getString(anyString())).thenReturn(mockVpnString); + + when(vpnExecutor.checkIp(anyString())).thenReturn(CompletableFuture.completedFuture( + VPNResponse.builder().success(true).proxy(false).ip("127.0.0.1") + .method("N/A").countryName("N/A").city("N/A").build() + )); + + // Use reflection to set the private static INSTANCE field + Field instanceField = AntiVPN.class.getDeclaredField("INSTANCE"); + instanceField.setAccessible(true); + instanceField.set(null, antiVPN); + + listener = new BukkitListener(); + } + + @AfterEach + public void tearDown() throws Exception { + // Reset the singleton + Field instanceField = AntiVPN.class.getDeclaredField("INSTANCE"); + instanceField.setAccessible(true); + instanceField.set(null, null); + + MockBukkit.unmock(); + } + + @Test + public void testLoginEventAllowed() throws Exception { + PlayerMock player = server.addPlayer("TestPlayer"); + InetAddress address = InetAddress.getByName("127.0.0.1"); + + PlayerLoginEvent event = new PlayerLoginEvent(player, "localhost", address); + + listener.onLogin(event); + + assertEquals(PlayerLoginEvent.Result.ALLOWED, event.getResult()); + } + + @Test + public void testLoginEventBlocked() throws Exception { + PlayerMock player = server.addPlayer("ProxyPlayer"); + InetAddress address = InetAddress.getByName("1.1.1.1"); + + // Mock proxy response + when(vpnExecutor.checkIp("1.1.1.1")).thenReturn(CompletableFuture.completedFuture( + VPNResponse.builder().success(true).proxy(true).ip("1.1.1.1") + .method("N/A").countryName("N/A").countryCode("N/A").city("N/A").build() + )); + + PlayerLoginEvent event = new PlayerLoginEvent(player, "localhost", address); + + listener.onLogin(event); + + assertEquals(PlayerLoginEvent.Result.KICK_BANNED, event.getResult()); + assertEquals("Blocked!", net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().serialize(event.kickMessage())); + } +} diff --git a/Bungee/BungeePlugin/build.gradle b/Bungee/BungeePlugin/build.gradle index f752a6d..50fa42f 100644 --- a/Bungee/BungeePlugin/build.gradle +++ b/Bungee/BungeePlugin/build.gradle @@ -4,9 +4,21 @@ plugins { dependencies { compileOnly 'net.md-5:bungeecord-api:1.21-R0.2' - compileOnly project(path: ':Common:Source', configuration: 'shadow') - compileOnly project(':Common:loader-utils') + testImplementation 'net.md-5:bungeecord-api:1.21-R0.2' + implementation project(':Common:Source') + implementation project(':Common:loader-utils') implementation 'org.bstats:bstats-bungeecord:2.2.1' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4' + testImplementation 'org.mockito:mockito-core:5.11.0' + testImplementation 'org.mockito:mockito-subclass:5.11.0' + testImplementation 'org.mockito:mockito-junit-jupiter:5.11.0' + testImplementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' +} +tasks.compileJava.dependsOn(':Common:Source:jar') + +test { + useJUnitPlatform() + systemProperty 'mockito.mockmaker', 'subclass' } shadowJar { diff --git a/Bungee/BungeePlugin/src/test/java/dev/brighten/antivpn/bungee/BungeeListenerTest.java b/Bungee/BungeePlugin/src/test/java/dev/brighten/antivpn/bungee/BungeeListenerTest.java new file mode 100644 index 0000000..0358da3 --- /dev/null +++ b/Bungee/BungeePlugin/src/test/java/dev/brighten/antivpn/bungee/BungeeListenerTest.java @@ -0,0 +1,110 @@ +package dev.brighten.antivpn.bungee; + +import dev.brighten.antivpn.AntiVPN; +import dev.brighten.antivpn.api.PlayerExecutor; +import dev.brighten.antivpn.api.VPNConfig; +import dev.brighten.antivpn.api.VPNExecutor; +import dev.brighten.antivpn.message.MessageHandler; +import dev.brighten.antivpn.message.VpnString; +import dev.brighten.antivpn.web.objects.VPNResponse; +import net.md_5.bungee.api.connection.PendingConnection; +import net.md_5.bungee.api.event.PreLoginEvent; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import static org.mockito.Mockito.*; + +public class BungeeListenerTest { + + private BungeeListener listener; + private VPNExecutor vpnExecutor; + + @BeforeEach + public void setUp() throws Exception { + AntiVPN antiVPN = mock(AntiVPN.class); + VPNConfig config = mock(VPNConfig.class); + PlayerExecutor playerExecutor = mock(PlayerExecutor.class); + vpnExecutor = mock(VPNExecutor.class); + MessageHandler messageHandler = mock(MessageHandler.class); + + when(antiVPN.getVpnConfig()).thenReturn(config); + when(antiVPN.getPlayerExecutor()).thenReturn(playerExecutor); + when(antiVPN.getExecutor()).thenReturn(vpnExecutor); + when(antiVPN.getMessageHandler()).thenReturn(messageHandler); + + when(playerExecutor.getPlayer(any(UUID.class))).thenReturn(Optional.empty()); + when(config.getPrefixWhitelists()).thenReturn(java.util.Collections.emptyList()); + when(config.getCountryList()).thenReturn(java.util.Collections.emptyList()); + when(config.isKickPlayers()).thenReturn(true); + when(config.getKickMessage()).thenReturn("Blocked!"); + + VpnString mockVpnString = mock(VpnString.class); + when(mockVpnString.getFormattedMessage(any())).thenReturn("Blocked!"); + when(messageHandler.getString(anyString())).thenReturn(mockVpnString); + + when(vpnExecutor.checkIp(anyString())).thenReturn(CompletableFuture.completedFuture( + VPNResponse.builder().success(true).proxy(false).ip("127.0.0.1") + .method("N/A").countryName("N/A").city("N/A").build() + )); + + // Use reflection to set the private static INSTANCE field + Field instanceField = AntiVPN.class.getDeclaredField("INSTANCE"); + instanceField.setAccessible(true); + instanceField.set(null, antiVPN); + + listener = new BungeeListener(); + } + + @AfterEach + public void tearDown() throws Exception { + // Reset the singleton + Field instanceField = AntiVPN.class.getDeclaredField("INSTANCE"); + instanceField.setAccessible(true); + instanceField.set(null, null); + } + + @Test + public void testPreLoginEventAllowed() { + PreLoginEvent event = mock(PreLoginEvent.class); + PendingConnection connection = mock(PendingConnection.class); + + when(event.getConnection()).thenReturn(connection); + when(connection.getUniqueId()).thenReturn(UUID.randomUUID()); + when(connection.getName()).thenReturn("TestPlayer"); + when(connection.getSocketAddress()).thenReturn(new InetSocketAddress("127.0.0.1", 12345)); + + listener.onListener(event); + + verify(event, never()).setCancelled(true); + } + + @Test + public void testPreLoginEventBlocked() { + PreLoginEvent event = mock(PreLoginEvent.class); + PendingConnection connection = mock(PendingConnection.class); + + UUID uuid = UUID.randomUUID(); + when(event.getConnection()).thenReturn(connection); + when(connection.getUniqueId()).thenReturn(uuid); + when(connection.getName()).thenReturn("ProxyPlayer"); + when(connection.getSocketAddress()).thenReturn(new InetSocketAddress("1.1.1.1", 12345)); + + // Mock proxy response + when(vpnExecutor.checkIp("1.1.1.1")).thenReturn(CompletableFuture.completedFuture( + VPNResponse.builder().success(true).proxy(true).ip("1.1.1.1") + .method("N/A").countryName("N/A").countryCode("N/A").city("N/A").build() + )); + + listener.onListener(event); + + verify(event).setCancelled(true); + verify(event).setReason(any()); + } +} diff --git a/Common/Source/build.gradle b/Common/Source/build.gradle index bc89fd7..6040092 100644 --- a/Common/Source/build.gradle +++ b/Common/Source/build.gradle @@ -12,10 +12,18 @@ dependencies { compileOnly 'com.h2database:h2:2.2.220' compileOnly 'com.github.ben-manes.caffeine:caffeine:3.1.8' compileOnly 'org.mongodb:mongo-java-driver:3.12.14' -} - -jar { - enabled = false + + testImplementation 'org.mockito:mockito-core:5.11.0' + testImplementation "org.junit.jupiter:junit-jupiter:5.8.1" + testImplementation "org.testcontainers:testcontainers:2.0.4" + testImplementation "org.testcontainers:testcontainers-junit-jupiter:2.0.4" + testImplementation 'org.testcontainers:mysql:1.20.4' + testImplementation 'org.testcontainers:mongodb:1.20.4' + testRuntimeOnly 'org.slf4j:slf4j-simple:2.0.16' + testImplementation 'com.mysql:mysql-connector-j:9.3.0' + testImplementation 'com.h2database:h2:2.2.220' + testImplementation 'org.mongodb:mongo-java-driver:3.12.14' + testImplementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' } shadowJar { @@ -38,3 +46,12 @@ tasks.build.dependsOn shadowJar components.java.withVariantsFromConfiguration(configurations.shadowRuntimeElements) { skip() } + + +test { + useJUnitPlatform() +} + +jar { + archiveClassifier.set('raw') +} diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/AntiVPN.java b/Common/Source/src/main/java/dev/brighten/antivpn/AntiVPN.java index 569076c..56a39f7 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/AntiVPN.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/AntiVPN.java @@ -206,8 +206,12 @@ public class AntiVPN { executor.log("Failed to deregister H2 driver: " + e.getMessage()); } } - AntiVPN.getInstance().getExecutor().getThreadExecutor().shutdown(); + if (executor != null && executor.getThreadExecutor() != null) { + executor.getThreadExecutor().shutdown(); + } if(database != null) database.shutdown(); + + INSTANCE = null; } public void reloadDatabase() { diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/api/VPNExecutor.java b/Common/Source/src/main/java/dev/brighten/antivpn/api/VPNExecutor.java index 6a9f870..12a8137 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/api/VPNExecutor.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/api/VPNExecutor.java @@ -16,6 +16,8 @@ package dev.brighten.antivpn.api; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; import dev.brighten.antivpn.AntiVPN; import dev.brighten.antivpn.utils.CIDRUtils; import dev.brighten.antivpn.utils.StringUtil; @@ -146,7 +148,18 @@ public abstract class VPNExecutor { } } + private final Cache cachedResponses = Caffeine.newBuilder() + .expireAfterWrite(20, TimeUnit.MINUTES) + .maximumSize(4000) + .build(); + public CompletableFuture checkIp(String ip) { + VPNResponse cached = cachedResponses.getIfPresent(ip); + + if(cached != null) { + return CompletableFuture.completedFuture(cached); + } + return CompletableFuture.supplyAsync(() -> { Optional cachedRes = AntiVPN.getInstance().getDatabase().getStoredResponse(ip); diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/database/local/H2VPN.java b/Common/Source/src/main/java/dev/brighten/antivpn/database/local/H2VPN.java index a9dae1b..1fd5bce 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/database/local/H2VPN.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/database/local/H2VPN.java @@ -16,8 +16,6 @@ package dev.brighten.antivpn.database.local; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; import dev.brighten.antivpn.AntiVPN; import dev.brighten.antivpn.database.VPNDatabase; import dev.brighten.antivpn.database.sql.utils.ExecutableStatement; @@ -40,11 +38,6 @@ import java.util.function.Consumer; public class H2VPN implements VPNDatabase { - private final Cache cachedResponses = Caffeine.newBuilder() - .expireAfterWrite(20, TimeUnit.MINUTES) - .maximumSize(4000) - .build(); - public H2VPN() { AntiVPN.getInstance().getExecutor().getThreadExecutor().scheduleAtFixedRate(() -> { @@ -67,29 +60,25 @@ public class H2VPN implements VPNDatabase { if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled()|| MySQL.isClosed()) return Optional.empty(); - VPNResponse response = cachedResponses.get(ip, ip2 -> { - try(ExecutableStatement statement = Query.prepare("select * from `responses` where `ip` = ? limit 1").append(ip)) { - try(ResultSet rs = statement.executeQuery()) { - if (rs != null && rs.next()) { - return new VPNResponse(rs.getString("asn"), rs.getString("ip"), - rs.getString("countryName"), rs.getString("countryCode"), - rs.getString("city"), rs.getString("timeZone"), - rs.getString("method"), rs.getString("isp"), "N/A", - rs.getBoolean("proxy"), rs.getBoolean("cached"), true, - rs.getDouble("latitude"), rs.getDouble("longitude"), - rs.getTimestamp("inserted").getTime(), -1); - } + try(ExecutableStatement statement = Query.prepare("select * from `responses` where `ip` = ? limit 1").append(ip)) { + try(ResultSet rs = statement.executeQuery()) { + if (rs != null && rs.next()) { + return Optional.of(new VPNResponse(rs.getString("asn"), rs.getString("ip"), + rs.getString("countryName"), rs.getString("countryCode"), + rs.getString("city"), rs.getString("timeZone"), + rs.getString("method"), rs.getString("isp"), "N/A", + rs.getBoolean("proxy"), rs.getBoolean("cached"), true, + rs.getDouble("latitude"), rs.getDouble("longitude"), + rs.getTimestamp("inserted").getTime(), -1)); } - } catch (SQLException e) { - AntiVPN.getInstance().getExecutor().logException("There was a problem getting a response for " - + ip, e); - } catch (Exception e) { - throw new RuntimeException(e); } - return null; - }); - - return Optional.ofNullable(response); + } catch (SQLException e) { + AntiVPN.getInstance().getExecutor().logException("There was a problem getting a response for " + + ip, e); + } catch (Exception e) { + throw new RuntimeException(e); + } + return Optional.empty(); } /* @@ -106,20 +95,16 @@ public class H2VPN implements VPNDatabase { if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed()) return; - if(AntiVPN.getInstance().getVpnConfig().cachedResults()) { - cachedResponses.put(toCache.getIp(), toCache); - - try(var statement = Query.prepare("insert into `responses` (`ip`,`asn`,`countryName`,`countryCode`,`city`,`timeZone`," - + "`method`,`isp`,`proxy`,`cached`,`inserted`,`latitude`,`longitude`) values (?,?,?,?,?,?,?,?,?,?,?,?,?)") - .append(toCache.getIp()).append(toCache.getAsn()).append(toCache.getCountryName()) - .append(toCache.getCountryCode()).append(toCache.getCity()).append(toCache.getTimeZone()) - .append(toCache.getMethod()).append(toCache.getIsp()).append(toCache.isProxy()) - .append(toCache.isCached()).append(new Timestamp(System.currentTimeMillis())) - .append(toCache.getLatitude()).append(toCache.getLongitude())) { - statement.execute(); - } catch(SQLException e) { - AntiVPN.getInstance().getExecutor().logException("Could not cache response for IP: " + toCache.getIp(), e); - } + try(var statement = Query.prepare("insert into `responses` (`ip`,`asn`,`countryName`,`countryCode`,`city`,`timeZone`," + + "`method`,`isp`,`proxy`,`cached`,`inserted`,`latitude`,`longitude`) values (?,?,?,?,?,?,?,?,?,?,?,?,?)") + .append(toCache.getIp()).append(toCache.getAsn()).append(toCache.getCountryName()) + .append(toCache.getCountryCode()).append(toCache.getCity()).append(toCache.getTimeZone()) + .append(toCache.getMethod()).append(toCache.getIsp()).append(toCache.isProxy()) + .append(toCache.isCached()).append(new Timestamp(System.currentTimeMillis())) + .append(toCache.getLatitude()).append(toCache.getLongitude())) { + statement.execute(); + } catch(SQLException e) { + AntiVPN.getInstance().getExecutor().logException("Could not cache response for IP: " + toCache.getIp(), e); } } @@ -139,6 +124,7 @@ public class H2VPN implements VPNDatabase { public boolean isWhitelisted(UUID uuid) { if (!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled() || MySQL.isClosed()) return false; + try(var statement = Query.prepare("select uuid from `whitelisted` where `uuid` = ? limit 1") .append(uuid.toString())) { try(var set = statement.executeQuery()) { diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/database/local/version/First.java b/Common/Source/src/main/java/dev/brighten/antivpn/database/local/version/First.java index 8f7064a..7b78bbb 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/database/local/version/First.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/database/local/version/First.java @@ -52,11 +52,11 @@ public class First implements Version { .append(versionNumber())).execute(); AntiVPN.getInstance().getExecutor().log("Creating indexes..."); - createIndexIfAbsent("whitelisted", "uuid_1", "`uuid`"); - createIndexIfAbsent("responses", "ip_1", "`ip`"); - createIndexIfAbsent("responses", "proxy_1", "`proxy`"); - createIndexIfAbsent("responses", "inserted_1", "`inserted`"); - createIndexIfAbsent("whitelisted-ips", "ip_1", "`ip`"); + createIndexIfAbsent("whitelisted", "whitelisted_uuid_1", "`uuid`"); + createIndexIfAbsent("responses", "responses_ip_1", "`ip`"); + createIndexIfAbsent("responses", "responses_proxy_1", "`proxy`"); + createIndexIfAbsent("responses", "responses_inserted_1", "`inserted`"); + createIndexIfAbsent("whitelisted-ips", "whitelisted_ips_ip_1", "`ip`"); } catch (SQLException e) { throw new DatabaseException("Failed to update database", e); } finally { diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/database/local/version/Second.java b/Common/Source/src/main/java/dev/brighten/antivpn/database/local/version/Second.java index 55c9927..fd81080 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/database/local/version/Second.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/database/local/version/Second.java @@ -86,6 +86,7 @@ public class Second extends First implements Version { } dropIndexIfPresent("whitelisted-ips", "ip_1"); + dropIndexIfPresent("whitelisted-ips", "whitelisted_ips_ip_1"); closeOnEnd(Query.prepare("DROP TABLE `whitelisted-ips`")).execute(); closeOnEnd(Query.prepare("INSERT INTO `database_version` (`version`) VALUES (?)").append(versionNumber())).execute(); } catch (Throwable e) { @@ -123,7 +124,7 @@ public class Second extends First implements Version { statement.execute(); } - createIndexIfAbsent("whitelisted-ips", "ip_1", "`ip`"); + createIndexIfAbsent("whitelisted-ips", "whitelisted_ips_ip_1", "`ip`"); try(var statement = Query.prepare("DELETE FROM `whitelisted-ips`")) { statement.execute(); diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/database/mongo/MongoVPN.java b/Common/Source/src/main/java/dev/brighten/antivpn/database/mongo/MongoVPN.java index 87fb166..bb2b806 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/database/mongo/MongoVPN.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/database/mongo/MongoVPN.java @@ -47,11 +47,6 @@ public class MongoVPN implements VPNDatabase { private MongoClient client; public MongoDatabase antivpnDatabase; - private final Cache cachedResponses = Caffeine.newBuilder() - .expireAfterWrite(20, TimeUnit.MINUTES) - .maximumSize(4000) - .build(); - public MongoVPN() { AntiVPN.getInstance().getExecutor().getThreadExecutor().scheduleAtFixedRate(() -> { if(!AntiVPN.getInstance().getVpnConfig().isDatabaseEnabled()) return; @@ -69,37 +64,32 @@ public class MongoVPN implements VPNDatabase { } @Override public Optional getStoredResponse(String ip) { - VPNResponse response = cachedResponses.get(ip, ip2 -> { - Document rdoc = cacheDocument.find(Filters.eq("ip", ip)).first(); + Document rdoc = cacheDocument.find(Filters.eq("ip", ip)).first(); - if(rdoc != null) { - long lastUpdate = rdoc.get("lastAccess", 0L); + if(rdoc != null) { + long lastUpdate = rdoc.get("lastAccess", 0L); - if(System.currentTimeMillis() - lastUpdate > TimeUnit.HOURS.toMillis(1)) { - AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> deleteResponse(ip)); - return null; - } - - return VPNResponse.builder().asn(rdoc.getString("asn")).ip(ip) - .countryName(rdoc.getString("countryName")) - .countryCode(rdoc.getString("countryCode")) - .city(rdoc.getString("city")) - .isp(rdoc.getString("isp")) - .method(rdoc.getString("method")) - .timeZone(rdoc.getString("timeZone")) - .proxy(rdoc.getBoolean("proxy")) - .cached(rdoc.getBoolean("cached")) - .success(true) - .latitude(rdoc.getDouble("latitude")) - .longitude(rdoc.getDouble("longitude")) - .lastAccess(rdoc.get("lastAccess", 0L)) - .build(); + if(System.currentTimeMillis() - lastUpdate > TimeUnit.HOURS.toMillis(1)) { + AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> deleteResponse(ip)); + return null; } - return null; - }); - - return Optional.ofNullable(response); + return Optional.of(VPNResponse.builder().asn(rdoc.getString("asn")).ip(ip) + .countryName(rdoc.getString("countryName")) + .countryCode(rdoc.getString("countryCode")) + .city(rdoc.getString("city")) + .isp(rdoc.getString("isp")) + .method(rdoc.getString("method")) + .timeZone(rdoc.getString("timeZone")) + .proxy(rdoc.getBoolean("proxy")) + .cached(rdoc.getBoolean("cached")) + .success(true) + .latitude(rdoc.getDouble("latitude")) + .longitude(rdoc.getDouble("longitude")) + .lastAccess(rdoc.get("lastAccess", 0L)) + .build()); + } + return Optional.empty(); } @Override @@ -121,8 +111,6 @@ public class MongoVPN implements VPNDatabase { rdoc.put("longitude", toCache.getLongitude()); rdoc.put("lastAccess", System.currentTimeMillis()); - cachedResponses.put(toCache.getIp(), toCache); - AntiVPN.getInstance().getExecutor().getThreadExecutor().execute(() -> { Bson update = new Document("$set", rdoc); cacheDocument.updateOne(Filters.eq("ip", toCache.getIp()), update, diff --git a/Common/Source/src/main/java/dev/brighten/antivpn/database/sql/utils/MySQL.java b/Common/Source/src/main/java/dev/brighten/antivpn/database/sql/utils/MySQL.java index c88bfe2..662ef70 100644 --- a/Common/Source/src/main/java/dev/brighten/antivpn/database/sql/utils/MySQL.java +++ b/Common/Source/src/main/java/dev/brighten/antivpn/database/sql/utils/MySQL.java @@ -26,6 +26,7 @@ import java.io.IOException; import java.nio.file.Files; import java.sql.Connection; import java.sql.SQLException; +import java.sql.SQLSyntaxErrorException; import java.util.Properties; import java.util.logging.Level; @@ -48,9 +49,21 @@ public class MySQL { } conn.setAutoCommit(true); Query.use(conn); - Query.prepare("CREATE DATABASE IF NOT EXISTS `" - + AntiVPN.getInstance().getVpnConfig().getDatabaseName() + "`").execute(); - Query.prepare("USE `" + AntiVPN.getInstance().getVpnConfig().getDatabaseName() + "`").execute(); + String databaseName = AntiVPN.getInstance().getVpnConfig().getDatabaseName(); + + try { + Query.prepare("CREATE DATABASE IF NOT EXISTS `" + databaseName + "`").execute(); + } catch (SQLException ex) { + if (!isDatabaseCreationPermissionIssue(ex)) { + throw ex; + } + + AntiVPN.getInstance().getExecutor().log( + "No permission to create MySQL database `" + databaseName + + "`. Attempting to use the existing database instead."); + } + + Query.prepare("USE `" + databaseName + "`").execute(); AntiVPN.getInstance().getExecutor().log("Connection to MySQL has been established."); } } catch (Exception e) { @@ -59,6 +72,12 @@ public class MySQL { } } + private static boolean isDatabaseCreationPermissionIssue(SQLException ex) { + return ex instanceof SQLSyntaxErrorException + && ex.getMessage() != null + && ex.getMessage().contains("Access denied"); + } + public static void initH2() { initH2(true); } diff --git a/Common/Source/src/test/java/dev/brighten/antivpn/database/DatabaseIntegrationTestSupport.java b/Common/Source/src/test/java/dev/brighten/antivpn/database/DatabaseIntegrationTestSupport.java new file mode 100644 index 0000000..cdf97c6 --- /dev/null +++ b/Common/Source/src/test/java/dev/brighten/antivpn/database/DatabaseIntegrationTestSupport.java @@ -0,0 +1,217 @@ +package dev.brighten.antivpn.database; + +import dev.brighten.antivpn.AntiVPN; +import dev.brighten.antivpn.api.VPNConfig; +import dev.brighten.antivpn.api.VPNExecutor; +import dev.brighten.antivpn.database.sql.utils.MySQL; +import dev.brighten.antivpn.utils.CIDRUtils; +import dev.brighten.antivpn.web.objects.VPNResponse; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +abstract class DatabaseIntegrationTestSupport { + + @TempDir + Path pluginFolder; + + @Mock + protected AntiVPN antiVPN; + + @Mock + protected VPNConfig vpnConfig; + + protected TestVPNExecutor vpnExecutor; + private AutoCloseable mocks; + private final AtomicReference activeDatabase = new AtomicReference<>(); + + @BeforeEach + void setUpBase() throws Exception { + mocks = MockitoAnnotations.openMocks(this); + vpnExecutor = new TestVPNExecutor(); + + when(antiVPN.getVpnConfig()).thenReturn(vpnConfig); + when(antiVPN.getExecutor()).thenReturn(vpnExecutor); + when(antiVPN.getPluginFolder()).thenReturn(pluginFolder.toFile()); + when(antiVPN.getDatabase()).thenAnswer(invocation -> activeDatabase.get()); + + lenient().when(vpnConfig.isDatabaseEnabled()).thenReturn(true); + lenient().when(vpnConfig.cachedResults()).thenReturn(true); + lenient().when(vpnConfig.getUsername()).thenReturn("testuser"); + lenient().when(vpnConfig.getPassword()).thenReturn("testpass"); + lenient().when(vpnConfig.getDatabaseName()).thenReturn("antivpn"); + lenient().when(vpnConfig.getIp()).thenReturn("127.0.0.1"); + lenient().when(vpnConfig.getPort()).thenReturn(-1); + lenient().when(vpnConfig.mongoDatabaseURL()).thenReturn(""); + lenient().when(vpnConfig.useDatabaseCreds()).thenReturn(false); + + setAntiVpnInstance(antiVPN); + } + + @AfterEach + void tearDownBase() throws Exception { + VPNDatabase database = activeDatabase.getAndSet(null); + if (database != null) { + database.shutdown(); + } + + MySQL.shutdown(); + if (vpnExecutor != null) { + vpnExecutor.getThreadExecutor().shutdownNow(); + vpnExecutor.getThreadExecutor().awaitTermination(5, TimeUnit.SECONDS); + } + + setAntiVpnInstance(null); + + if (mocks != null) { + mocks.close(); + } + } + + protected void registerDatabase(VPNDatabase database) { + activeDatabase.set(database); + } + + protected void assertDatabaseContract(VPNDatabase database) throws Exception { + registerDatabase(database); + database.init(); + + VPNResponse response = VPNResponse.builder() + .ip("1.2.3.4") + .asn("AS123") + .countryName("United States") + .countryCode("US") + .city("New York") + .proxy(true) + .cached(true) + .success(true) + .build(); + + database.cacheResponse(response); + + Optional storedResponse = awaitStoredResponse(database, response.getIp()); + assertTrue(storedResponse.isPresent(), "Expected cached response to be stored"); + assertEquals("AS123", storedResponse.get().getAsn()); + assertTrue(storedResponse.get().isProxy()); + + database.deleteResponse(response.getIp()); + awaitCondition(() -> database.getStoredResponse(response.getIp()).isEmpty(), + "Expected cached response to be deleted"); + + database.cacheResponse(response); + awaitCondition(() -> database.getStoredResponse(response.getIp()).isPresent(), + "Expected cached response to be restored"); + + UUID uuid = UUID.randomUUID(); + assertFalse(database.isWhitelisted(uuid)); + database.addWhitelist(uuid); + awaitCondition(() -> database.isWhitelisted(uuid), "Expected UUID whitelist entry to exist"); + List whitelisted = database.getAllWhitelisted(); + assertTrue(whitelisted.contains(uuid)); + database.removeWhitelist(uuid); + awaitCondition(() -> !database.isWhitelisted(uuid), "Expected UUID whitelist entry to be removed"); + + CIDRUtils cidr = new CIDRUtils("192.168.1.0/24"); + assertFalse(database.isWhitelisted(cidr)); + database.addWhitelist(cidr); + awaitCondition(() -> database.isWhitelisted(cidr), "Expected CIDR whitelist entry to exist"); + List whitelistedIps = database.getAllWhitelistedIps(); + assertTrue(whitelistedIps.stream().anyMatch(entry -> entry.getCidr().equals(cidr.getCidr()))); + database.removeWhitelist(cidr); + awaitCondition(() -> !database.isWhitelisted(cidr), "Expected CIDR whitelist entry to be removed"); + + database.updateAlertsState(uuid, true); + awaitCondition(() -> awaitAlertsState(database, uuid), "Expected alerts to be enabled"); + database.updateAlertsState(uuid, false); + awaitCondition(() -> !awaitAlertsState(database, uuid), "Expected alerts to be disabled"); + + database.clearResponses(); + awaitCondition(() -> database.getStoredResponse(response.getIp()).isEmpty(), + "Expected cached responses to be cleared"); + } + + private Optional awaitStoredResponse(VPNDatabase database, String ip) throws InterruptedException { + AtomicReference> result = new AtomicReference<>(Optional.empty()); + awaitCondition(() -> { + Optional response = database.getStoredResponse(ip); + result.set(response); + return response.isPresent(); + }, "Timed out waiting for cached response"); + return result.get(); + } + + private boolean awaitAlertsState(VPNDatabase database, UUID uuid) throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + AtomicReference result = new AtomicReference<>(false); + database.alertsState(uuid, enabled -> { + result.set(enabled); + latch.countDown(); + }); + assertTrue(latch.await(2, TimeUnit.SECONDS), "Timed out waiting for alerts state callback"); + return result.get(); + } + + protected void awaitCondition(CheckedBooleanSupplier condition, String failureMessage) throws InterruptedException { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); + while (System.nanoTime() < deadline) { + try { + if (condition.getAsBoolean()) { + return; + } + } catch (Exception e) { + fail(e.getMessage(), e); + return; + } + Thread.sleep(100); + } + fail(failureMessage); + } + + private static void setAntiVpnInstance(AntiVPN instance) throws Exception { + Field instanceField = AntiVPN.class.getDeclaredField("INSTANCE"); + instanceField.setAccessible(true); + instanceField.set(null, instance); + } + + @FunctionalInterface + protected interface CheckedBooleanSupplier { + boolean getAsBoolean() throws Exception; + } + + protected static final class TestVPNExecutor extends VPNExecutor { + @Override + public void registerListeners() {} + + @Override + public void log(Level level, String log, Object... objects) {} + + @Override + public void log(String log, Object... objects) {} + + @Override + public void logException(String message, Throwable ex) {} + + @Override + public void runCommand(String command) {} + + @Override + public void disablePlugin() {} + } +} diff --git a/Common/Source/src/test/java/dev/brighten/antivpn/database/H2DatabaseIntegrationTest.java b/Common/Source/src/test/java/dev/brighten/antivpn/database/H2DatabaseIntegrationTest.java new file mode 100644 index 0000000..1e22f64 --- /dev/null +++ b/Common/Source/src/test/java/dev/brighten/antivpn/database/H2DatabaseIntegrationTest.java @@ -0,0 +1,12 @@ +package dev.brighten.antivpn.database; + +import dev.brighten.antivpn.database.local.H2VPN; +import org.junit.jupiter.api.Test; + +class H2DatabaseIntegrationTest extends DatabaseIntegrationTestSupport { + + @Test + void h2DatabaseImplementsTheVpnDatabaseContract() throws Exception { + assertDatabaseContract(new H2VPN()); + } +} diff --git a/Common/Source/src/test/java/dev/brighten/antivpn/database/MongoDatabaseIntegrationTest.java b/Common/Source/src/test/java/dev/brighten/antivpn/database/MongoDatabaseIntegrationTest.java new file mode 100644 index 0000000..133c593 --- /dev/null +++ b/Common/Source/src/test/java/dev/brighten/antivpn/database/MongoDatabaseIntegrationTest.java @@ -0,0 +1,40 @@ +package dev.brighten.antivpn.database; + +import com.mongodb.client.MongoClients; +import dev.brighten.antivpn.database.mongo.MongoVPN; +import org.junit.jupiter.api.Test; +import org.bson.Document; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@Testcontainers +class MongoDatabaseIntegrationTest extends DatabaseIntegrationTestSupport { + + @Container + private static final MongoDBContainer MONGO = new MongoDBContainer("mongo:6.0.14"); + + @Test + void mongoDatabaseImplementsTheVpnDatabaseContract() throws Exception { + assertTrue(MONGO.isRunning(), "Mongo Testcontainer should be running"); + + try (var client = MongoClients.create(MONGO.getConnectionString())) { + var response = client.getDatabase("admin").runCommand(new Document("ping", 1)); + assertEquals(1.0d, response.getDouble("ok"), "Expected Mongo container to respond to ping"); + } + + when(vpnConfig.getIp()).thenReturn(MONGO.getHost()); + when(vpnConfig.getPort()).thenReturn(MONGO.getMappedPort(27017)); + when(vpnConfig.getDatabaseName()).thenReturn("antivpn_" + UUID.randomUUID().toString().replace("-", "")); + when(vpnConfig.mongoDatabaseURL()).thenReturn(""); + when(vpnConfig.useDatabaseCreds()).thenReturn(false); + + assertDatabaseContract(new MongoVPN()); + } +} diff --git a/Common/Source/src/test/java/dev/brighten/antivpn/database/MySqlDatabaseIntegrationTest.java b/Common/Source/src/test/java/dev/brighten/antivpn/database/MySqlDatabaseIntegrationTest.java new file mode 100644 index 0000000..8e2898f --- /dev/null +++ b/Common/Source/src/test/java/dev/brighten/antivpn/database/MySqlDatabaseIntegrationTest.java @@ -0,0 +1,45 @@ +package dev.brighten.antivpn.database; + +import dev.brighten.antivpn.database.sql.MySqlVPN; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.sql.DriverManager; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@Testcontainers +class MySqlDatabaseIntegrationTest extends DatabaseIntegrationTestSupport { + + @Container + private static final MySQLContainer MYSQL = new MySQLContainer<>("mysql:8.0.36") + .withDatabaseName("antivpn") + .withUsername("testuser") + .withPassword("testpass"); + + @Test + void mysqlDatabaseImplementsTheVpnDatabaseContract() throws Exception { + assertTrue(MYSQL.isRunning(), "MySQL Testcontainer should be running"); + + try (var connection = DriverManager.getConnection(MYSQL.getJdbcUrl(), MYSQL.getUsername(), MYSQL.getPassword()); + var statement = connection.createStatement(); + var resultSet = statement.executeQuery("SELECT 1")) { + assertTrue(resultSet.next(), "Expected a row from the MySQL container"); + assertEquals(1, resultSet.getInt(1), "Expected MySQL container to respond to SELECT 1"); + } + + var pingResult = MYSQL.execInContainer("mysqladmin", "ping", "-h", "127.0.0.1", "-ptestpass"); + assertEquals(0, pingResult.getExitCode(), "Expected mysqladmin ping to succeed inside the container"); + + when(vpnConfig.getIp()).thenReturn(MYSQL.getHost()); + when(vpnConfig.getPort()).thenReturn(MYSQL.getMappedPort(3306)); + when(vpnConfig.getDatabaseName()).thenReturn(MYSQL.getDatabaseName()); + when(vpnConfig.getUsername()).thenReturn(MYSQL.getUsername()); + when(vpnConfig.getPassword()).thenReturn(MYSQL.getPassword()); + + assertDatabaseContract(new MySqlVPN()); + } +} diff --git a/Sponge/SpongePlugin/build.gradle b/Sponge/SpongePlugin/build.gradle index a9c2b27..3e50f0a 100644 --- a/Sponge/SpongePlugin/build.gradle +++ b/Sponge/SpongePlugin/build.gradle @@ -4,8 +4,22 @@ plugins { dependencies { compileOnly 'org.spongepowered:spongeapi:11.0.0' - compileOnly project(path: ':Common:Source', configuration: 'shadow') + testImplementation 'org.spongepowered:spongeapi:11.0.0' + compileOnly project(':Common:Source') + testImplementation project(':Common:Source') compileOnly project(':Common:loader-utils') + testImplementation project(':Common:loader-utils') + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4' + testImplementation 'org.mockito:mockito-core:5.11.0' + testImplementation 'org.mockito:mockito-subclass:5.11.0' + testImplementation 'org.mockito:mockito-junit-jupiter:5.11.0' + testImplementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' +} +tasks.compileJava.dependsOn(':Common:Source:jar') + +test { + useJUnitPlatform() + systemProperty 'mockito.mockmaker', 'subclass' } shadowJar { diff --git a/Sponge/SpongePlugin/src/test/java/dev/brighten/antivpn/sponge/SpongeListenerTest.java b/Sponge/SpongePlugin/src/test/java/dev/brighten/antivpn/sponge/SpongeListenerTest.java new file mode 100644 index 0000000..f90e9c4 --- /dev/null +++ b/Sponge/SpongePlugin/src/test/java/dev/brighten/antivpn/sponge/SpongeListenerTest.java @@ -0,0 +1,114 @@ +package dev.brighten.antivpn.sponge; + +import dev.brighten.antivpn.AntiVPN; +import dev.brighten.antivpn.api.PlayerExecutor; +import dev.brighten.antivpn.api.VPNConfig; +import dev.brighten.antivpn.api.VPNExecutor; +import dev.brighten.antivpn.message.MessageHandler; +import dev.brighten.antivpn.message.VpnString; +import dev.brighten.antivpn.web.objects.VPNResponse; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.spongepowered.api.event.network.ServerSideConnectionEvent; +import org.spongepowered.api.network.ServerSideConnection; +import org.spongepowered.api.profile.GameProfile; + +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import static org.mockito.Mockito.*; + +public class SpongeListenerTest { + + private SpongeListener listener; + private VPNExecutor vpnExecutor; + + @BeforeEach + public void setUp() throws Exception { + AntiVPN antiVPN = mock(AntiVPN.class); + VPNConfig config = mock(VPNConfig.class); + PlayerExecutor playerExecutor = mock(PlayerExecutor.class); + vpnExecutor = mock(VPNExecutor.class); + MessageHandler messageHandler = mock(MessageHandler.class); + + when(antiVPN.getVpnConfig()).thenReturn(config); + when(antiVPN.getPlayerExecutor()).thenReturn(playerExecutor); + when(antiVPN.getExecutor()).thenReturn(vpnExecutor); + when(antiVPN.getMessageHandler()).thenReturn(messageHandler); + + when(playerExecutor.getPlayer(any(UUID.class))).thenReturn(Optional.empty()); + when(config.getPrefixWhitelists()).thenReturn(java.util.Collections.emptyList()); + when(config.getCountryList()).thenReturn(java.util.Collections.emptyList()); + when(config.isKickPlayers()).thenReturn(true); + when(config.getKickMessage()).thenReturn("Blocked!"); + + VpnString mockVpnString = mock(VpnString.class); + when(mockVpnString.getFormattedMessage(any())).thenReturn("Blocked!"); + when(messageHandler.getString(anyString())).thenReturn(mockVpnString); + + when(vpnExecutor.checkIp(anyString())).thenReturn(CompletableFuture.completedFuture( + VPNResponse.builder().success(true).proxy(false).ip("127.0.0.1") + .method("N/A").countryName("N/A").city("N/A").build() + )); + + // Use reflection to set the private static INSTANCE field + Field instanceField = AntiVPN.class.getDeclaredField("INSTANCE"); + instanceField.setAccessible(true); + instanceField.set(null, antiVPN); + + listener = new SpongeListener(); + } + + @AfterEach + public void tearDown() throws Exception { + // Reset the singleton + Field instanceField = AntiVPN.class.getDeclaredField("INSTANCE"); + instanceField.setAccessible(true); + instanceField.set(null, null); + } + + @Test + public void testLoginEventAllowed() { + ServerSideConnectionEvent.Login event = mock(ServerSideConnectionEvent.Login.class); + GameProfile profile = mock(GameProfile.class); + ServerSideConnection connection = mock(ServerSideConnection.class); + + when(event.profile()).thenReturn(profile); + when(event.connection()).thenReturn(connection); + when(profile.uuid()).thenReturn(UUID.randomUUID()); + when(profile.name()).thenReturn(Optional.of("TestPlayer")); + when(connection.address()).thenReturn(new InetSocketAddress("127.0.0.1", 12345)); + + listener.onJoin(event); + + verify(event, never()).setCancelled(true); + } + + @Test + public void testLoginEventBlocked() { + ServerSideConnectionEvent.Login event = mock(ServerSideConnectionEvent.Login.class); + GameProfile profile = mock(GameProfile.class); + ServerSideConnection connection = mock(ServerSideConnection.class); + + when(event.profile()).thenReturn(profile); + when(event.connection()).thenReturn(connection); + when(profile.uuid()).thenReturn(UUID.randomUUID()); + when(profile.name()).thenReturn(Optional.of("ProxyPlayer")); + when(connection.address()).thenReturn(new InetSocketAddress("1.1.1.1", 12345)); + + // Mock proxy response + when(vpnExecutor.checkIp("1.1.1.1")).thenReturn(CompletableFuture.completedFuture( + VPNResponse.builder().success(true).proxy(true).ip("1.1.1.1") + .method("N/A").countryName("N/A").countryCode("N/A").city("N/A").build() + )); + + listener.onJoin(event); + + verify(event).setCancelled(true); + verify(event).setMessage(any()); + } +} diff --git a/Velocity/VelocityPlugin/build.gradle b/Velocity/VelocityPlugin/build.gradle index 6754670..03f9d5b 100644 --- a/Velocity/VelocityPlugin/build.gradle +++ b/Velocity/VelocityPlugin/build.gradle @@ -4,9 +4,23 @@ plugins { dependencies { compileOnly 'com.velocitypowered:velocity-api:3.4.0-SNAPSHOT' - compileOnly project(path: ':Common:Source', configuration: 'shadow') + testImplementation 'com.velocitypowered:velocity-api:3.4.0-SNAPSHOT' + compileOnly project(':Common:Source') compileOnly project(':Common:loader-utils') implementation 'org.bstats:bstats-velocity:2.2.1' + testImplementation 'org.mockito:mockito-core:5.11.0' + testImplementation 'org.mockito:mockito-junit-jupiter:5.11.0' + testImplementation 'net.java.dev.jna:jna:5.14.0' + testImplementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4' + testImplementation project(':Common:Source') + testImplementation project(':Common:loader-utils') +} +tasks.compileJava.dependsOn(':Common:Source:jar') + +test { + useJUnitPlatform() + jvmArgs("-XX:+EnableDynamicAgentLoading") } shadowJar { diff --git a/Velocity/VelocityPlugin/src/main/java/dev/brighten/antivpn/velocity/VelocityListener.java b/Velocity/VelocityPlugin/src/main/java/dev/brighten/antivpn/velocity/VelocityListener.java index 66e0c50..35d4cab 100644 --- a/Velocity/VelocityPlugin/src/main/java/dev/brighten/antivpn/velocity/VelocityListener.java +++ b/Velocity/VelocityPlugin/src/main/java/dev/brighten/antivpn/velocity/VelocityListener.java @@ -41,43 +41,45 @@ public class VelocityListener extends VPNExecutor { .unloadPlayer(event.getPlayer().getUniqueId())); VelocityPlugin.INSTANCE.getServer().getEventManager().register(VelocityPlugin.INSTANCE.getPluginInstance(), LoginEvent.class, - event -> { - APIPlayer player = AntiVPN.getInstance().getPlayerExecutor().getPlayer(event.getPlayer().getUniqueId()) - .orElse(new OfflinePlayer( - event.getPlayer().getUniqueId(), - event.getPlayer().getUsername(), - event.getPlayer().getRemoteAddress().getAddress() - )); + this::onLogin); + } - player.checkPlayer(result -> { - if(!result.resultType().isShouldBlock()) return; + public void onLogin(LoginEvent event) { + APIPlayer player = AntiVPN.getInstance().getPlayerExecutor().getPlayer(event.getPlayer().getUniqueId()) + .orElse(new OfflinePlayer( + event.getPlayer().getUniqueId(), + event.getPlayer().getUsername(), + event.getPlayer().getRemoteAddress().getAddress() + )); - if(!AntiVPN.getInstance().getVpnConfig().isKickPlayers()) { - return; - } + player.checkPlayer(result -> { + if(!result.resultType().isShouldBlock()) return; - switch (result.resultType()) { - case DENIED_COUNTRY -> event.setResult(ResultedEvent.ComponentResult.denied( - LegacyComponentSerializer.builder() - .character('&') - .build().deserialize(AntiVPN.getInstance().getVpnConfig() - .getCountryVanillaKickReason() - .replace("%player%", event.getPlayer().getUsername()) - .replace("%country%", result.response().getCountryName()) - .replace("%code%", result.response().getCountryCode())))); - case DENIED_PROXY -> { - VelocityPlugin.INSTANCE.getLogger().info(event.getPlayer().getUsername() - + " joined on a VPN/Proxy (" + result.response().getMethod() + ")"); - event.setResult(ResultedEvent.ComponentResult.denied(LegacyComponentSerializer.builder() + if(!AntiVPN.getInstance().getVpnConfig().isKickPlayers()) { + return; + } + + switch (result.resultType()) { + case DENIED_COUNTRY -> event.setResult(ResultedEvent.ComponentResult.denied( + LegacyComponentSerializer.builder() .character('&') .build().deserialize(AntiVPN.getInstance().getVpnConfig() - .getKickMessage() + .getCountryVanillaKickReason() .replace("%player%", event.getPlayer().getUsername()) .replace("%country%", result.response().getCountryName()) .replace("%code%", result.response().getCountryCode())))); - } + case DENIED_PROXY -> { + VelocityPlugin.INSTANCE.getLogger().info(event.getPlayer().getUsername() + + " joined on a VPN/Proxy (" + result.response().getMethod() + ")"); + event.setResult(ResultedEvent.ComponentResult.denied(LegacyComponentSerializer.builder() + .character('&') + .build().deserialize(AntiVPN.getInstance().getVpnConfig() + .getKickMessage() + .replace("%player%", event.getPlayer().getUsername()) + .replace("%country%", result.response().getCountryName()) + .replace("%code%", result.response().getCountryCode())))); } - }); + } }); } diff --git a/Velocity/VelocityPlugin/src/test/java/dev/brighten/antivpn/velocity/VelocityListenerTest.java b/Velocity/VelocityPlugin/src/test/java/dev/brighten/antivpn/velocity/VelocityListenerTest.java new file mode 100644 index 0000000..9a71225 --- /dev/null +++ b/Velocity/VelocityPlugin/src/test/java/dev/brighten/antivpn/velocity/VelocityListenerTest.java @@ -0,0 +1,126 @@ +package dev.brighten.antivpn.velocity; + +import com.velocitypowered.api.event.connection.LoginEvent; +import com.velocitypowered.api.proxy.Player; +import dev.brighten.antivpn.AntiVPN; +import dev.brighten.antivpn.api.PlayerExecutor; +import dev.brighten.antivpn.api.VPNConfig; +import dev.brighten.antivpn.api.VPNExecutor; +import dev.brighten.antivpn.message.MessageHandler; +import dev.brighten.antivpn.message.VpnString; +import dev.brighten.antivpn.web.objects.VPNResponse; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import java.util.logging.Logger; + +import static org.mockito.Mockito.*; + +public class VelocityListenerTest { + + private VelocityListener listener; + private AntiVPN antiVPN; + private VPNConfig config; + private PlayerExecutor playerExecutor; + private VPNExecutor vpnExecutor; + private MessageHandler messageHandler; + private VelocityPlugin velocityPlugin; + + @BeforeEach + public void setUp() throws Exception { + antiVPN = mock(AntiVPN.class); + config = mock(VPNConfig.class); + playerExecutor = mock(PlayerExecutor.class); + vpnExecutor = mock(VPNExecutor.class); + messageHandler = mock(MessageHandler.class); + velocityPlugin = mock(VelocityPlugin.class); + + when(antiVPN.getVpnConfig()).thenReturn(config); + when(antiVPN.getPlayerExecutor()).thenReturn(playerExecutor); + when(antiVPN.getExecutor()).thenReturn(vpnExecutor); + when(antiVPN.getMessageHandler()).thenReturn(messageHandler); + + when(velocityPlugin.getLogger()).thenReturn(Logger.getLogger("AntiVPN")); + + when(playerExecutor.getPlayer(any(UUID.class))).thenReturn(Optional.empty()); + when(config.getPrefixWhitelists()).thenReturn(java.util.Collections.emptyList()); + when(config.getCountryList()).thenReturn(java.util.Collections.emptyList()); + when(config.isKickPlayers()).thenReturn(true); + when(config.getKickMessage()).thenReturn("Blocked!"); + + VpnString mockVpnString = mock(VpnString.class); + when(mockVpnString.getFormattedMessage(any())).thenReturn("Blocked!"); + when(messageHandler.getString(anyString())).thenReturn(mockVpnString); + + when(vpnExecutor.checkIp(anyString())).thenReturn(CompletableFuture.completedFuture( + VPNResponse.builder().success(true).proxy(false).ip("127.0.0.1") + .method("N/A").countryName("N/A").city("N/A").countryCode("N/A").build() + )); + + // Use reflection to set the private static INSTANCE field + Field instanceField = AntiVPN.class.getDeclaredField("INSTANCE"); + instanceField.setAccessible(true); + instanceField.set(null, antiVPN); + + Field pluginInstanceField = VelocityPlugin.class.getDeclaredField("INSTANCE"); + pluginInstanceField.setAccessible(true); + pluginInstanceField.set(null, velocityPlugin); + + listener = new VelocityListener(); + } + + @AfterEach + public void tearDown() throws Exception { + // Reset the singletons + Field instanceField = AntiVPN.class.getDeclaredField("INSTANCE"); + instanceField.setAccessible(true); + instanceField.set(null, null); + + Field pluginInstanceField = VelocityPlugin.class.getDeclaredField("INSTANCE"); + pluginInstanceField.setAccessible(true); + pluginInstanceField.set(null, null); + } + + @Test + public void testLoginEventAllowed() throws Exception { + LoginEvent event = mock(LoginEvent.class); + Player player = mock(Player.class); + + when(event.getPlayer()).thenReturn(player); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + when(player.getUsername()).thenReturn("TestPlayer"); + when(player.getRemoteAddress()).thenReturn(new InetSocketAddress("127.0.0.1", 12345)); + + listener.onLogin(event); + + verify(event, never()).setResult(any()); + } + + @Test + public void testLoginEventBlocked() throws Exception { + LoginEvent event = mock(LoginEvent.class); + Player player = mock(Player.class); + + when(event.getPlayer()).thenReturn(player); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + when(player.getUsername()).thenReturn("ProxyPlayer"); + when(player.getRemoteAddress()).thenReturn(new InetSocketAddress("1.1.1.1", 12345)); + + // Mock proxy response + when(vpnExecutor.checkIp("1.1.1.1")).thenReturn(CompletableFuture.completedFuture( + VPNResponse.builder().success(true).proxy(true).ip("1.1.1.1") + .method("N/A").countryName("N/A").city("N/A").countryCode("N/A").build() + )); + + listener.onLogin(event); + + verify(event).setResult(any()); + } +} diff --git a/build.gradle b/build.gradle index 57472fa..daf06ef 100644 --- a/build.gradle +++ b/build.gradle @@ -3,14 +3,24 @@ plugins { id 'com.gradleup.shadow' version '9.4.1' } +def aggregateTestProjects = [ + project(':Common:Source'), + project(':Bukkit:Plugin'), + project(':Bungee:BungeePlugin'), + project(':Sponge:SpongePlugin'), + project(':Velocity:VelocityPlugin') +] + allprojects { group = 'dev.brighten.antivpn' version = '1.10.0' repositories { - maven {url 'https://nexus.funkemunky.cc/repository/maven-central/'} - maven { url 'https://nexus.funkemunky.cc/content/repositories/releases/' } + maven { url 'https://repo.papermc.io/repository/maven-public/' } maven { url 'https://nexus.funkemunky.cc/repository/papermc-public/' } + maven { url 'https://nexus.funkemunky.cc/repository/maven-public/' } + maven { url 'https://nexus.funkemunky.cc/repository/maven-central/' } + maven { url 'https://nexus.funkemunky.cc/content/repositories/releases/' } maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' } maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } maven { url 'https://repo.spongepowered.org/repository/maven-public/' } @@ -33,7 +43,30 @@ allprojects { dependencies { compileOnly 'org.projectlombok:lombok:1.18.44' annotationProcessor 'org.projectlombok:lombok:1.18.44' + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } + + test { + useJUnitPlatform() + systemProperty 'mockito.mockmaker', 'subclass' + } +} + +dependencies { + testImplementation 'org.junit.platform:junit-platform-suite:1.11.4' +} + +sourceSets { + test { + compileClasspath += files(aggregateTestProjects.collect { it.sourceSets.test.output + it.sourceSets.test.compileClasspath }) + runtimeClasspath += files(aggregateTestProjects.collect { it.sourceSets.test.output + it.sourceSets.test.runtimeClasspath }) + } +} + +tasks.named('test') { + dependsOn(aggregateTestProjects.collect { it.tasks.named('testClasses') }) + jvmArgs("-XX:+EnableDynamicAgentLoading") } evaluationDependsOn(':Common:Source') diff --git a/gradle.properties b/gradle.properties index 3dc33e2..e69de29 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +0,0 @@ -systemProp.javax.net.ssl.trustStore=NONE -systemProp.javax.net.ssl.trustStoreType=Windows-ROOT diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/settings.gradle b/settings.gradle index ebda19f..10c35cd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,3 +15,4 @@ include 'Sponge:SpongeLoader' include 'Velocity:VelocityPlugin' include 'Velocity:VelocityLoader' +