diff --git a/tests/keycloak.rs b/tests/keycloak.rs new file mode 100644 index 0000000..6490748 --- /dev/null +++ b/tests/keycloak.rs @@ -0,0 +1,272 @@ +use testcontainers::{ + core::{CmdWaitFor, ExecCommand, Image, WaitFor}, + runners::AsyncRunner, + ContainerAsync, +}; + +#[derive(Debug, Default, Clone)] +struct KeycloakImage; + +const NAME: &str = "quay.io/keycloak/keycloak"; +const TAG: &str = "25.0"; + +impl Image for KeycloakImage { + fn name(&self) -> &str { + NAME + } + + fn tag(&self) -> &str { + TAG + } + + fn ready_conditions(&self) -> Vec { + vec![WaitFor::message_on_stdout("Listening on:"), + WaitFor::message_on_stdout("Running the server in development mode. DO NOT use this configuration in production.") + ] + } + + fn env_vars( + &self, + ) -> impl IntoIterator< + Item = ( + impl Into>, + impl Into>, + ), + > { + [ + ("KEYCLOAK_ADMIN", "admin"), + ("KEYCLOAK_ADMIN_PASSWORD", "admin"), + ] + } + + fn cmd(&self) -> impl IntoIterator>> { + ["start-dev"] + } +} + +pub struct Keycloak { + container: ContainerAsync, + realms: Vec, + url: String, +} + +#[derive(Clone)] +pub struct Realm { + pub name: String, + pub clients: Vec, + pub users: Vec, +} + +#[derive(Clone)] +pub struct Client { + pub client_id: String, + pub client_secret: Option, +} + +impl Default for Client { + fn default() -> Self { + Self { + client_id: "0".to_owned(), + client_secret: None, + } + } +} + +#[derive(Clone)] +pub struct User { + pub username: String, + pub email: String, + pub firstname: String, + pub lastname: String, + pub password: String, +} + +impl Keycloak { + pub async fn start( + realms: Vec, + ) -> Result> { + let container = KeycloakImage.start().await?; + + let keycloak = Self { + url: format!( + "http://localhost:{}", + container.get_host_port_ipv4(8080).await?, + ), + container, + realms, + }; + + keycloak + .container + .exec( + ExecCommand::new([ + "/opt/keycloak/bin/kcadm.sh", + "config", + "credentials", + "--server", + "http://localhost:8080", + "--realm", + "master", + "--user", + "admin", + "--password", + "admin", + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + ) + .await + .unwrap(); + + for realm in keycloak.realms.iter() { + if realm.name != "master" { + keycloak.create_realm(&realm.name).await; + } + for client in realm.clients.iter() { + keycloak + .create_client( + &client.client_id, + client.client_secret.as_deref(), + &realm.name, + ) + .await; + } + for user in realm.users.iter() { + keycloak + .create_user( + &user.username, + &user.email, + &user.firstname, + &user.lastname, + &user.password, + &realm.name, + ) + .await; + } + } + + Ok(keycloak) + } + + pub fn url(&self) -> &str { + &self.url + } + + pub async fn create_realm(&self, name: &str) { + self.container + .exec( + ExecCommand::new([ + "/opt/keycloak/bin/kcadm.sh", + "create", + "realms", + "-s", + &format!("realm={name}"), + "-s", + "enabled=true", + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + ) + .await + .unwrap(); + } + + pub async fn create_client(&self, client_id: &str, client_secret: Option<&str>, realm: &str) { + if let Some(client_secret) = client_secret { + self.container + .exec( + ExecCommand::new([ + "/opt/keycloak/bin/kcadm.sh", + "create", + "clients", + "-r", + &realm, + "-s", + &format!("clientId={client_id}"), + "-s", + &format!("secret={client_secret}"), + "-s", + "redirectUris=[\"*\"]", + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + ) + .await + .unwrap(); + } else { + self.container + .exec( + ExecCommand::new([ + "/opt/keycloak/bin/kcadm.sh", + "create", + "clients", + "-r", + &realm, + "-s", + &format!("clientId={client_id}"), + "-s", + "redirectUris=[\"*\"]", + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + ) + .await + .unwrap(); + } + } + + pub async fn create_user( + &self, + username: &str, + email: &str, + firstname: &str, + lastname: &str, + password: &str, + realm: &str, + ) -> String { + let stderr = self.container + .exec( + ExecCommand::new([ + "/opt/keycloak/bin/kcadm.sh", + "create", + "users", + "-r", + &realm, + "-s", + &format!("username={username}"), + "-s", + "enabled=true", + "-s", + "emailVerified=true", + "-s", + &format!("email={email}"), + "-s", + &format!("firstName={firstname}"), + "-s", + &format!("lastName={lastname}"), + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + ) + .await + .unwrap() + .stderr_to_vec() + .await.unwrap(); + + let stderr = String::from_utf8_lossy(&stderr); + let id = stderr.split('\'').nth(1).unwrap().to_string(); + + self.container + .exec( + ExecCommand::new([ + "/opt/keycloak/bin/kcadm.sh", + "set-password", + "-r", + &realm, + "--username", + username, + "--new-password", + password, + ]) + .with_cmd_ready_condition(CmdWaitFor::exit_code(0)), + ) + .await + .unwrap(); + id + } +}