Added testing for databases, and fixed some potential database bugs

This commit is contained in:
2026-04-08 21:01:29 -04:00
parent 3583b15815
commit ae14755205
23 changed files with 962 additions and 128 deletions
@@ -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<VPNDatabase> 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<VPNResponse> 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<UUID> 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<CIDRUtils> 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<VPNResponse> awaitStoredResponse(VPNDatabase database, String ip) throws InterruptedException {
AtomicReference<Optional<VPNResponse>> result = new AtomicReference<>(Optional.empty());
awaitCondition(() -> {
Optional<VPNResponse> 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<Boolean> 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() {}
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -0,0 +1,47 @@
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 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 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("antivpn_" + UUID.randomUUID().toString().replace("-", ""));
when(vpnConfig.getUsername()).thenReturn(MYSQL.getUsername());
when(vpnConfig.getPassword()).thenReturn(MYSQL.getPassword());
assertDatabaseContract(new MySqlVPN());
}
}