Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
359 changes: 82 additions & 277 deletions .github/workflows/e2e.yml

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ e2e/tests/*.log
e2e/tests/coverage/
e2e/jars/

# E2E runtime-generated files (LuckPerms libs, plugin data, etc.)
e2e/platforms/**/luckperms/libs/
e2e/platforms/**/luckperms/*.json

dependency-reduced-pom.xml

# Fabric/Loom
Expand Down
7 changes: 4 additions & 3 deletions bungee/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,12 @@ tasks.named<ShadowJar>("shadowJar") {
dependencies {
include(dependency(":BanManagerWebEnhancerCommon"))
include(dependency(":BanManagerWebEnhancerLibs"))
relocate("org.bstats", "me.confuser.banmanager.webenhancer.common.bstats") {
include(dependency("org.bstats:"))
}
include(dependency("org.bstats:bstats-bungeecord:.*"))
include(dependency("org.bstats:bstats-base:.*"))
}

relocate("org.bstats", "me.confuser.banmanager.webenhancer.common.bstats")

exclude("GradleStart**")
exclude(".cache");
exclude("LICENSE*")
Expand Down
15 changes: 15 additions & 0 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
`java-library`
jacoco
}

applyPlatformAndCoreConfiguration()
Expand All @@ -23,4 +24,18 @@ tasks.withType<Test>().configureEach {
useJUnit()
maxHeapSize = "512m"
forkEvery = 1 // Fork a new JVM for each test class to prevent memory accumulation
finalizedBy(tasks.jacocoTestReport)
}

jacoco {
toolVersion = "0.8.11"
}

tasks.jacocoTestReport {
dependsOn(tasks.test)
reports {
xml.required.set(true)
html.required.set(true)
html.outputLocation.set(layout.buildDirectory.dir("reports/jacoco"))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package me.confuser.banmanager.webenhancer.common.data;

import me.confuser.banmanager.common.data.PlayerData;
import org.junit.Test;

import java.security.NoSuchAlgorithmException;
import java.util.UUID;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

/**
* Unit tests for PlayerPinData.
*/
public class PlayerPinDataTest {

@Test
public void shouldGenerateSixDigitPin() throws NoSuchAlgorithmException {
PlayerData player = mock(PlayerData.class);
when(player.getUUID()).thenReturn(UUID.randomUUID());

PlayerPinData pinData = new PlayerPinData(player);
int pin = pinData.getGeneratedPin();

// 6 digit pin: 100000-999999
assertTrue("Pin should be at least 100000", pin >= 100000);
assertTrue("Pin should be at most 999999", pin <= 999999);
}

@Test
public void shouldHashPinWithArgon2() throws NoSuchAlgorithmException {
PlayerData player = mock(PlayerData.class);
when(player.getUUID()).thenReturn(UUID.randomUUID());

PlayerPinData pinData = new PlayerPinData(player);
String hashedPin = pinData.getPin();

assertNotNull("Hashed pin should not be null", hashedPin);
assertTrue("Pin should be hashed with Argon2i", hashedPin.startsWith("$argon2i$"));
}

@Test
public void shouldSetExpiryFiveMinutesFromNow() throws NoSuchAlgorithmException {
PlayerData player = mock(PlayerData.class);
when(player.getUUID()).thenReturn(UUID.randomUUID());

long beforeCreate = System.currentTimeMillis() / 1000L;
PlayerPinData pinData = new PlayerPinData(player);
long afterCreate = System.currentTimeMillis() / 1000L;

// Expiry should be 300 seconds (5 minutes) from creation
long expires = pinData.getExpires();

// Allow 1 second tolerance for test execution
assertTrue("Expiry should be ~300 seconds from creation",
expires >= beforeCreate + 299 && expires <= afterCreate + 301);
}

@Test
public void shouldStoreGeneratedPinForDisplay() throws NoSuchAlgorithmException {
PlayerData player = mock(PlayerData.class);
when(player.getUUID()).thenReturn(UUID.randomUUID());

PlayerPinData pinData = new PlayerPinData(player);

int generatedPin = pinData.getGeneratedPin();
String hashedPin = pinData.getPin();

assertNotEquals("Generated pin should not be 0", 0, generatedPin);
assertFalse("Hashed pin should not equal plaintext",
hashedPin.equals(String.valueOf(generatedPin)));
}

@Test
public void shouldStorePlayerReference() throws NoSuchAlgorithmException {
PlayerData player = mock(PlayerData.class);
when(player.getUUID()).thenReturn(UUID.randomUUID());

PlayerPinData pinData = new PlayerPinData(player);

assertSame("Player reference should be stored", player, pinData.getPlayer());
}

@Test
public void shouldHaveZeroIdBeforePersistence() throws NoSuchAlgorithmException {
PlayerData player = mock(PlayerData.class);
when(player.getUUID()).thenReturn(UUID.randomUUID());

PlayerPinData pinData = new PlayerPinData(player);

assertEquals("ID should be 0 before persistence", 0, pinData.getId());
}

@Test
public void shouldGenerateRandomPins() throws NoSuchAlgorithmException {
PlayerData player = mock(PlayerData.class);
when(player.getUUID()).thenReturn(UUID.randomUUID());

PlayerPinData pin1 = new PlayerPinData(player);
PlayerPinData pin2 = new PlayerPinData(player);
PlayerPinData pin3 = new PlayerPinData(player);

boolean atLeastTwoDifferent =
pin1.getGeneratedPin() != pin2.getGeneratedPin() ||
pin1.getGeneratedPin() != pin3.getGeneratedPin() ||
pin2.getGeneratedPin() != pin3.getGeneratedPin();

assertTrue("At least two of three pins should differ", atLeastTwoDifferent);
}

@Test
public void shouldGenerateUniqueHashesForSamePin() throws NoSuchAlgorithmException {
PlayerData player = mock(PlayerData.class);
when(player.getUUID()).thenReturn(UUID.randomUUID());

PlayerPinData pin1 = new PlayerPinData(player);
PlayerPinData pin2 = new PlayerPinData(player);

// Even if by chance the generated pins are the same, the hashes should differ
// due to unique random salts in Argon2
assertNotEquals("Hashes should differ due to random salts",
pin1.getPin(), pin2.getPin());
}

@Test
public void generatedPinShouldBeSettable() throws NoSuchAlgorithmException {
PlayerData player = mock(PlayerData.class);
when(player.getUUID()).thenReturn(UUID.randomUUID());

PlayerPinData pinData = new PlayerPinData(player);
pinData.setGeneratedPin(999999);

assertEquals("Generated pin should be settable", 999999, pinData.getGeneratedPin());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,8 @@ public void handlePin_replacesPlaceholderWithPin() {
Message message = mock(Message.class);
when(message.toString()).thenReturn("Your login pin is: [pin]");

// Execute
listener.handlePin(player, message);

// Verify - message.set() should be called with the pin
verify(message).set("pin", "123456");
}

Expand All @@ -60,13 +58,9 @@ public void handlePin_ignoresMessagesWithoutPlaceholder() {
Message message = mock(Message.class);
when(message.toString()).thenReturn("You have been banned!");

// Execute
listener.handlePin(player, message);

// Verify - getValidPin should NOT be called since message doesn't contain [pin]
verify(playerPinStorage, never()).getValidPin(any());

// Verify set was never called
verify(message, never()).set(anyString(), anyString());
}

Expand All @@ -79,10 +73,8 @@ public void handlePin_handlesNullPinGracefully() {
Message message = mock(Message.class);
when(message.toString()).thenReturn("Your login pin is: [pin]");

// Execute - should not throw
listener.handlePin(player, message);

// Verify - set should NOT be called when pin is null
verify(message, never()).set(anyString(), anyString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package me.confuser.banmanager.webenhancer.common.security;

import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.*;

/**
* Unit tests for Argon2PasswordEncoder.
*/
public class Argon2PasswordEncoderTest {

private Argon2PasswordEncoder encoder;

@Before
public void setUp() {
encoder = new Argon2PasswordEncoder();
}

@Test
public void shouldEncodePassword() {
String password = "123456";

String encoded = encoder.encode(password);

assertNotNull("Encoded password should not be null", encoded);
assertNotEquals("Encoded password should differ from original", password, encoded);
}

@Test
public void shouldProduceArgon2iFormat() {
String password = "testpassword";

String encoded = encoder.encode(password);

// Argon2i format starts with $argon2i$
assertTrue("Hash should use Argon2i format", encoded.startsWith("$argon2i$"));
}

@Test
public void shouldGenerateUniqueSaltsForSameInput() {
String password = "samepassword";

String encoded1 = encoder.encode(password);
String encoded2 = encoder.encode(password);

assertNotEquals("Hashes should differ due to unique salts", encoded1, encoded2);
}

@Test
public void shouldProduceCorrectHashLength() {
String password = "test";

String encoded = encoder.encode(password);

assertTrue("Hash string should be substantial", encoded.length() > 50);
}

@Test
public void shouldHandleEmptyPassword() {
String password = "";

String encoded = encoder.encode(password);

assertNotNull("Should handle empty password", encoded);
assertTrue("Should still produce Argon2i format", encoded.startsWith("$argon2i$"));
}

@Test
public void shouldHandleLongPassword() {
StringBuilder longPassword = new StringBuilder();
for (int i = 0; i < 1000; i++) {
longPassword.append("a");
}

String encoded = encoder.encode(longPassword.toString());

assertNotNull("Should handle long password", encoded);
assertTrue("Should still produce Argon2i format", encoded.startsWith("$argon2i$"));
}

@Test
public void shouldHandleSpecialCharacters() {
String password = "p@$$w0rd!#$%^&*()日本語";

String encoded = encoder.encode(password);

assertNotNull("Should handle special characters", encoded);
assertTrue("Should still produce Argon2i format", encoded.startsWith("$argon2i$"));
}

@Test
public void shouldHandleNumericPin() {
// This is the most common use case - 6 digit PINs
String pin = "123456";

String encoded = encoder.encode(pin);

assertNotNull("Should handle numeric PIN", encoded);
assertTrue("Should produce Argon2i format", encoded.startsWith("$argon2i$"));

// Verify format contains expected parameters
assertTrue("Should contain version", encoded.contains("v="));
assertTrue("Should contain memory parameter", encoded.contains("m="));
assertTrue("Should contain time parameter", encoded.contains("t="));
assertTrue("Should contain parallelism parameter", encoded.contains("p="));
}

@Test
public void shouldProduceConsistentFormatAcrossMultipleCalls() {
Argon2PasswordEncoder encoder2 = new Argon2PasswordEncoder();

String encoded1 = encoder.encode("test");
String encoded2 = encoder2.encode("test");

String format1 = encoded1.substring(0, encoded1.indexOf("$", 9));

String format2 = encoded2.substring(0, encoded2.indexOf("$", 9));

assertEquals("Both instances should produce same format", format1, format2);
}
}
Loading
Loading