Skip to content

Testing secured apps

javalin-security is designed to be tested the way Javalin apps already are: with JavalinTest from javalin-testtools. JavalinTest starts a real server on a random port and gives you an HTTP client, so your tests assert the true end-to-end 200 / 401 / 403 behavior.

Test dependencies

testImplementation("io.javalin:javalin-testtools:7.2.2")
testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
testImplementation("org.assertj:assertj-core:3.27.3")

Build the app in a factory

Keep your Javalin configuration in a reusable factory so tests and production share it.

import io.github.mzlnk.javalin.security.authorization.Rules

val testAuthenticator = Authenticator { ctx ->
    when (ctx.header("X-User")) {
        null -> AuthenticationResult.NotAuthenticated
        "invalid" -> AuthenticationResult.Failure("bad")
        else -> AuthenticationResult.Success(
            Authentication.authenticated(ApiIdentity(ctx.header("X-User")!!, setOf(Role.ADMIN))),
        )
    }
}

fun secureApp(): Javalin = Javalin.create { config ->
    config.security { security ->
        security.rules.get("/public/*", Rules.allow())
        security.rules.post("/api/*", Rules.authenticated())
        security.rules.get("/admin/*", Rules.hasRole(Role.ADMIN))
        security.rules.ws("/ws/chat", Rules.authenticated())

        security.http.authentication = AuthenticationStrategy.sync(testAuthenticator)
        security.http.fallback = Rules.deny()
        security.ws.authentication = AuthenticationStrategy.sync(testAuthenticator)
        security.ws.fallback = Rules.deny()
    }

    config.routes.get("/public/info") { it.result("public") }
    config.routes.post("/api/data") { it.result("created") }
    config.routes.get("/admin/dashboard") { it.result("dashboard") }
    config.routes.ws("/ws/chat") { }
}
import io.github.mzlnk.javalin.security.authorization.Rules;

static Authenticator testAuthenticator = ctx -> {
    String user = ctx.header("X-User");
    if (user == null) return AuthenticationResult.NotAuthenticated.INSTANCE;
    if ("invalid".equals(user)) return new AuthenticationResult.Failure("bad", null);
    return new AuthenticationResult.Success(
        Authentication.authenticated(new ApiIdentity(user, Set.of(Role.ADMIN))));
};

static Javalin secureApp() {
    return Javalin.create(config -> {
        config.registerPlugin(new JavalinSecurityPlugin(security -> {
            security.rules.get("/public/*", Rules.allow());
            security.rules.post("/api/*", Rules.authenticated());
            security.rules.get("/admin/*", Rules.hasRole(Role.ADMIN));
            security.rules.ws("/ws/chat", Rules.authenticated());

            security.http.authentication = AuthenticationStrategy.sync(testAuthenticator);
            security.http.fallback = Rules.deny();
            security.ws.authentication = AuthenticationStrategy.sync(testAuthenticator);
            security.ws.fallback = Rules.deny();
        }));

        config.routes.get("/public/info", ctx -> ctx.result("public"));
        config.routes.post("/api/data", ctx -> ctx.result("created"));
        config.routes.get("/admin/dashboard", ctx -> ctx.result("dashboard"));
        config.routes.ws("/ws/chat", ws -> {});
    });
}

Write a test

Point JavalinTest at your factory and assert status codes like any other Javalin app:

import io.javalin.testtools.JavalinTest
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class SecurityTest {
    @Test fun `secured routes enforce authentication`() = JavalinTest.test(secureApp()) { _, client ->
        assertThat(client.get("/public/info").code).isEqualTo(200)
        assertThat(client.post("/api/data").code).isEqualTo(401)
        assertThat(client.post("/api/data", null) { it.header("X-User", "bob") }.code).isEqualTo(200)
    }
}
import io.javalin.testtools.JavalinTest;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;

class SecurityTest {
    @Test void secured_routes_enforce_authentication() {
        JavalinTest.test(secureApp(), (server, client) -> {
            assertThat(client.get("/public/info").code()).isEqualTo(200);
            assertThat(client.post("/api/data", "").code()).isEqualTo(401);
            assertThat(client.post("/api/data", "", req -> req.header("X-User", "bob")).code()).isEqualTo(200);
        });
    }
}

Testing WebSocket security

WebSocket authorization is enforced at upgrade time. Start the app with JavalinTest as usual, then assert the handshake status with any WebSocket client — the snippets below use the JDK HttpClient as an example.

A denied upgrade completes exceptionally with WebSocketHandshakeException. Read response.statusCode() for the 401 or 403:

import io.javalin.testtools.JavalinTest
import java.net.URI
import java.net.http.HttpClient
import java.net.http.WebSocket
import java.net.http.WebSocketHandshakeException
import java.util.concurrent.CompletionException
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

@Test
fun `anonymous upgrade is rejected`() = JavalinTest.test(secureApp()) { _, client ->
    val wsUri = URI.create("ws://127.0.0.1:${client.app.port()}/ws/chat")
    val future = HttpClient.newHttpClient()
        .newWebSocketBuilder()
        .buildAsync(wsUri, object : WebSocket.Listener {})

    val cause = runCatching { future.join() }.exceptionOrNull()
        .let { if (it is CompletionException) it.cause else it }

    assertThat(cause).isInstanceOf(WebSocketHandshakeException::class.java)
    assertThat((cause as WebSocketHandshakeException).response.statusCode()).isEqualTo(401)
}
import io.javalin.testtools.JavalinTest;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.net.http.WebSocketHandshakeException;
import java.util.concurrent.CompletionException;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

@Test
void anonymous_upgrade_is_rejected() {
    JavalinTest.test(secureApp(), (server, client) -> {
        URI wsUri = URI.create("ws://127.0.0.1:" + client.getApp().port() + "/ws/chat");
        var future = HttpClient.newHttpClient()
            .newWebSocketBuilder()
            .buildAsync(wsUri, new WebSocket.Listener() {});

        Throwable error = null;
        try {
            future.join();
        } catch (CompletionException e) {
            error = e.getCause() != null ? e.getCause() : e;
        }

        assertThat(error).isInstanceOf(WebSocketHandshakeException.class);
        assertThat(((WebSocketHandshakeException) error).getResponse().statusCode()).isEqualTo(401);
    });
}

Next steps