All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.justdevit.kotlin.boost.kotest.testcontainers.keycloak.KeycloakExtension.kt Maven / Gradle / Ivy

package io.justdevit.kotlin.boost.kotest.testcontainers.keycloak

import dasniko.testcontainers.keycloak.KeycloakContainer
import io.justdevit.kotlin.boost.extension.runIf
import io.justdevit.kotlin.boost.kotest.ANY_EXTENSION_FILTERS
import io.justdevit.kotlin.boost.kotest.AnnotationExtensionFilter
import io.justdevit.kotlin.boost.kotest.ExtensionFilter
import io.justdevit.kotlin.boost.kotest.ExternalToolExtension
import io.kotest.core.spec.Spec
import org.keycloak.admin.client.Keycloak
import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
import kotlin.reflect.KClass

/**
 * Lazy-initialized variable that represents the Keycloak admin client.
 * The admin client is obtained from the Keycloak container.
 */
val KEYCLOAK_ADMIN_CLIENT: Keycloak by lazy {
    keycloakContainer!!.keycloakAdminClient
}

internal var keycloakContainer: KeycloakContainer? = null
private val LOCK: Lock = ReentrantLock()

/**
 * The `KeycloakExtension` class is an implementation of the `ExternalToolExtension` interface.
 * It provides methods to instantiate a Keycloak container, mount it, and perform operations after the project.
 */
class KeycloakExtension(private val filters: Collection = ANY_EXTENSION_FILTERS) : ExternalToolExtension {

    constructor(vararg filters: ExtensionFilter) : this(filters.toSet())

    override fun  instantiate(clazz: KClass): Spec? {
        if (filters.any { it.decide(clazz) }) {
            startContainer()
        }
        return null
    }

    override fun mount(configure: KeycloakContainer.() -> Unit): Keycloak {
        startContainer()
        return keycloakContainer!!.keycloakAdminClient
    }

    override suspend fun afterProject() {
        keycloakContainer?.stop()
    }

    private fun startContainer() {
        LOCK.runIf({ keycloakContainer == null }) {
            keycloakContainer =
                KeycloakContainer("quay.io/keycloak/keycloak:latest").apply {
                    if (javaClass.classLoader.getResource("realm.json") != null) {
                        withRealmImportFile("/realm.json")
                    }
                    start()
                    waitingFor(HostPortWaitStrategy())
                    System.setProperty(KEYCLOAK_BASE_URL_PROPERTY, authServerUrl)
                }
            configureSystemProperties()
        }
    }

    private fun configureSystemProperties() {
        val realms = KEYCLOAK_REALMS
        System.setProperty(KEYCLOAK_REALMS_PROPERTY, realms.joinToString(separator = ","))
        if (realms.size == 1) {
            configureRealmIssuerUri(realms[0])
        } else {
            realms.forEach {
                configureRealmIssuerUri(it, it.toSystemPropertyPrefix())
            }
        }
    }

    private fun String.toSystemPropertyPrefix() =
        trim()
            .uppercase()
            .replace("-", "_")
            .replace(Regex("\\s+"), "_")

    private fun configureRealmIssuerUri(realm: String, prefix: String = "") {
        val fullPrefix = if (prefix.isBlank()) "" else "${prefix}_"
        System.setProperty("${fullPrefix}KEYCLOAK_ISSUER_URI", "${keycloakContainer!!.authServerUrl}/realms/$realm")
    }
}

/**
 * Creates a [KeycloakExtension] with the specified predicates for annotation [A].
 *
 * @param predicates The predicates used to filter the annotations.
 * @return The [KeycloakExtension] object.
 */
inline fun  KeycloakExtension(vararg predicates: (A) -> Boolean): KeycloakExtension {
    val filters = when {
        predicates.isEmpty() -> ANY_EXTENSION_FILTERS
        else -> predicates.map { AnnotationExtensionFilter(A::class, it) }
    }
    return KeycloakExtension(filters)
}

/**
 * Creates a [KeycloakExtension] with the specified predicate for annotation [A].
 *
 * @param predicate The predicate used to filter the annotations.
 * @return The [KeycloakExtension] object.
 */
inline fun  KeycloakExtension(noinline predicate: (A) -> Boolean) = KeycloakExtension(*arrayOf(predicate))




© 2015 - 2025 Weber Informatics LLC | Privacy Policy