diff --git a/bukkit/pom.xml b/bukkit/pom.xml
index 682c789b..8646be53 100644
--- a/bukkit/pom.xml
+++ b/bukkit/pom.xml
@@ -353,5 +353,40 @@
true
provided
+
+
+ org.testcontainers
+ testcontainers
+ 1.16.3
+ test
+
+
+
+ org.testcontainers
+ mockserver
+ 1.16.3
+ test
+
+
+
+ org.mock-server
+ mockserver-client-java
+ 5.11.2
+ test
+
+
+
+ com.github.steveice10
+ mcprotocollib
+ 1.18-2
+ test
+
+
+
+ ch.qos.logback
+ logback-core
+ 1.2.10
+ test
+
diff --git a/bukkit/src/test/java/integration/LoginIT.java b/bukkit/src/test/java/integration/LoginIT.java
new file mode 100644
index 00000000..96ff74d3
--- /dev/null
+++ b/bukkit/src/test/java/integration/LoginIT.java
@@ -0,0 +1,162 @@
+package integration;
+
+import com.github.steveice10.mc.protocol.MinecraftProtocol;
+import com.github.steveice10.packetlib.Session;
+import com.github.steveice10.packetlib.event.session.DisconnectedEvent;
+import com.github.steveice10.packetlib.event.session.SessionAdapter;
+import com.github.steveice10.packetlib.packet.Packet;
+import com.github.steveice10.packetlib.tcp.TcpClientSession;
+import com.google.common.io.CharStreams;
+
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockserver.client.MockServerClient;
+import org.mockserver.model.HttpRequest;
+import org.mockserver.verify.VerificationTimes;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.MockServerContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockserver.model.HttpRequest.request;
+import static org.mockserver.model.HttpResponse.response;
+
+// Warning name is sensitive to the surefire plugin
+public class LoginIT {
+
+ private static final String API_TAG = "mockserver-5.11.2";
+ private static final String API_IMAGE_NAME = "mockserver/mockserver";
+ private static final String API_IMAGE = API_IMAGE_NAME + ':' + API_TAG;
+
+ // @Rule
+ public MockServerContainer mockServer = new MockServerContainer(DockerImageName.parse(API_IMAGE))
+ .withReuse(true);
+
+ private static final String SERVER_TAG = "1.18.1";
+ private static final String SERVER_IMAGE_NAME = "ghcr.io/games647/paperclip";
+ private static final String SERVER_IMAGE = SERVER_IMAGE_NAME + ':' + SERVER_TAG;
+
+ @Rule
+ public GenericContainer> minecraftServer = new GenericContainer(DockerImageName.parse(SERVER_IMAGE))
+ .withEnv("JDK_JAVA_OPTIONS", "-Dcom.mojang.eula.agree=true")
+ .withExposedPorts(25565)
+ // Done (XXXXs)! For help, type "help"
+ .waitingFor(
+ Wait.forLogMessage(".*For help, type \"help\"*\\n", 1)
+ )
+ .withReuse(true);
+
+ @Test
+ public void checkRunning() throws Exception {
+ assertThat(minecraftServer.isRunning(), is(true));
+
+ String host = minecraftServer.getHost();
+ int port = minecraftServer.getMappedPort(25565);
+ Session clientSession = new TcpClientSession(host, port, new MinecraftProtocol());
+ try {
+ CompletableFuture connectionResult = new CompletableFuture<>();
+ clientSession.addListener(new SessionAdapter() {
+ @Override
+ public void packetReceived(Session session, Packet packet) {
+ System.out.println("Received: " + packet.getClass());
+ connectionResult.complete(true);
+ }
+
+ @Override
+ public void disconnected(DisconnectedEvent event) {
+ connectionResult.complete(false);
+ }
+ });
+
+ clientSession.connect();
+ assertThat(connectionResult.get(2, TimeUnit.SECONDS), is(true));
+ } finally {
+ clientSession.disconnect("Status test complete.");
+ }
+ }
+
+ @Test
+ public void autoRegisterNewUser() throws Exception {
+ assertThat(mockServer.isRunning(), is(true));
+
+ try (MockServerClient client = new MockServerClient(mockServer.getHost(), mockServer.getServerPort())) {
+ HttpRequest profileReq = request("/users/profiles/minecraft/" + "username");
+ HttpRequest hasJoinedReq = request()
+ .withPath("/session/minecraft/hasJoined")
+ .withQueryStringParameter("username", "")
+ .withQueryStringParameter("serverId", "")
+ .withQueryStringParameter("ip", "");
+
+ // check call network request times
+ client.verify(profileReq, VerificationTimes.once());
+ client.verify(hasJoinedReq, VerificationTimes.once());
+
+ // Verify order
+ client.verify(profileReq, hasJoinedReq);
+
+ client
+ .when(request()
+ .withPath("/users/profiles/minecraft/" + "username"))
+ .respond(response()
+ .withBody("bla"));
+
+ client
+ .when(hasJoinedReq)
+ .respond(response()
+ .withBody("Test"));
+
+ URLConnection urlConnection = new URL(mockServer.getEndpoint() + "/users/profiles/minecraft/username").openConnection();
+ String out = CharStreams.toString(new InputStreamReader(urlConnection.getInputStream(), StandardCharsets.UTF_8));
+ System.out.println("OUTPUT: " + out);
+ }
+ }
+
+ @Test
+ public void failedJoinedVerification() {
+ // has joined fails
+ }
+
+ @Test
+ public void offlineLoginNewUserDisabledRegister() {
+ // auto register disabled, always offline login for new users
+ }
+
+ @Test
+ public void offlineLoginNewUser() {
+ // auto register enabled, but no paid account
+ }
+
+ @Test
+ public void autoLoginRegistered() {
+ // registered premium user and paid account login in
+ }
+
+ @Test
+ public void failedLoginPremiumRegistered() {
+ // registered premium, but tried offline login
+ }
+
+ @Test
+ public void offlineLoginRegistered() {
+ // assume registered user marked as offline - tried to login
+ }
+
+ @Test
+ public void alreadyOnlineDuplicateOwner() {
+
+ }
+
+ @Test
+ public void alreadyOnlineDuplicateCracked() {
+
+ }
+}