From 00136320a92bc5f1bd767ddc92d0efff6f28aae1 Mon Sep 17 00:00:00 2001 From: pfzetto Date: Fri, 21 Nov 2025 13:47:37 +0100 Subject: [PATCH] removed integration test from `examples/basic` Integrations tests will be re-implemented on the main-crate. See #20 and #35. --- .github/workflows/ci.yml | 16 ++- examples/basic/Cargo.toml | 13 -- examples/basic/README.md | 2 - examples/basic/src/lib.rs | 84 ------------- examples/basic/src/main.rs | 87 ++++++++++++- examples/basic/tests/integration.rs | 94 -------------- examples/basic/tests/keycloak.rs | 188 ---------------------------- 7 files changed, 94 insertions(+), 390 deletions(-) delete mode 100644 examples/basic/src/lib.rs delete mode 100644 examples/basic/tests/integration.rs delete mode 100644 examples/basic/tests/keycloak.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee27b5e..b64f3dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,8 +10,8 @@ env: CARGO_TERM_COLOR: always jobs: - build_and_test: - name: axum-oidc - latest + test: + name: axum-oidc runs-on: ubuntu-latest strategy: matrix: @@ -24,13 +24,17 @@ jobs: - run: cargo build --verbose --release - run: cargo test --verbose --release - build_and_test_examples: - name: axum-oidc - examples + test_basic_example: + name: axum-oidc - basic runs-on: ubuntu-latest + strategy: + matrix: + toolchain: + - stable + - nightly steps: - uses: actions/checkout@v3 - - run: sudo apt install chromium-browser -y - - run: rustup update stable && rustup default stable + - run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} - run: cargo build --verbose --release working-directory: ./examples/basic - run: cargo test --verbose --release diff --git a/examples/basic/Cargo.toml b/examples/basic/Cargo.toml index bf2562f..88426a4 100644 --- a/examples/basic/Cargo.toml +++ b/examples/basic/Cargo.toml @@ -3,23 +3,10 @@ name = "basic" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] tokio = { version = "1.43", features = ["net", "macros", "rt-multi-thread"] } axum = { version = "0.8", features = [ "macros" ]} axum-oidc = { path = "./../.." } tower = "0.5" tower-sessions = "0.14" - dotenvy = "0.15" - -[dev-dependencies] -testcontainers = "0.23" -tokio = { version = "1.43", features = ["rt-multi-thread"] } -reqwest = { version = "0.12", features = ["rustls-tls"], default-features = false } -env_logger = "0.11" -log = "0.4" -headless_chrome = "1.0" -#see https://github.com/rust-headless-chrome/rust-headless-chrome/issues/535 -auto_generate_cdp = "=0.4.4" diff --git a/examples/basic/README.md b/examples/basic/README.md index 4011a45..30cdf2f 100644 --- a/examples/basic/README.md +++ b/examples/basic/README.md @@ -8,8 +8,6 @@ It has three endpoints: ## Dependencies You will need a running OpenID Connect capable issuer like [Keycloak](https://www.keycloak.org/getting-started/getting-started-docker) and a valid client for the issuer. -You can take a look at the `tests/`-folder to see how the automated keycloak deployment for the integration tests work. - ## Setup Environment Create a `.env`-file that contains the following keys: ``` diff --git a/examples/basic/src/lib.rs b/examples/basic/src/lib.rs deleted file mode 100644 index 8f7bbc2..0000000 --- a/examples/basic/src/lib.rs +++ /dev/null @@ -1,84 +0,0 @@ -use axum::{ - error_handling::HandleErrorLayer, - http::Uri, - response::IntoResponse, - routing::{any, get}, - Router, -}; -use axum_oidc::{ - error::MiddlewareError, handle_oidc_redirect, ClientId, ClientSecret, EmptyAdditionalClaims, - OidcAuthLayer, OidcClaims, OidcClient, OidcLoginLayer, OidcRpInitiatedLogout, -}; -use tokio::net::TcpListener; -use tower::ServiceBuilder; -use tower_sessions::{ - cookie::{time::Duration, SameSite}, - Expiry, MemoryStore, SessionManagerLayer, -}; - -pub async fn run(issuer: String, client_id: String, client_secret: Option) { - let session_store = MemoryStore::default(); - let session_layer = SessionManagerLayer::new(session_store) - .with_secure(false) - .with_same_site(SameSite::Lax) - .with_expiry(Expiry::OnInactivity(Duration::seconds(120))); - - let oidc_login_service = ServiceBuilder::new() - .layer(HandleErrorLayer::new(|e: MiddlewareError| async { - dbg!(&e); - e.into_response() - })) - .layer(OidcLoginLayer::::new()); - - let mut oidc_client = OidcClient::::builder() - .with_default_http_client() - .with_redirect_url(Uri::from_static("http://localhost:8080/oidc")) - .with_client_id(ClientId::new(client_id)); - if let Some(client_secret) = client_secret { - oidc_client = oidc_client.with_client_secret(ClientSecret::new(client_secret)); - } - let oidc_client = oidc_client.discover(issuer).await.unwrap().build(); - - let oidc_auth_service = ServiceBuilder::new() - .layer(HandleErrorLayer::new(|e: MiddlewareError| async { - dbg!(&e); - e.into_response() - })) - .layer(OidcAuthLayer::new(oidc_client)); - - let app = Router::new() - .route("/foo", get(authenticated)) - .route("/logout", get(logout)) - .layer(oidc_login_service) - .route("/bar", get(maybe_authenticated)) - .route("/oidc", any(handle_oidc_redirect::)) - .layer(oidc_auth_service) - .layer(session_layer); - - let listener = TcpListener::bind("[::]:8080").await.unwrap(); - axum::serve(listener, app.into_make_service()) - .await - .unwrap(); -} - -async fn authenticated(claims: OidcClaims) -> impl IntoResponse { - format!("Hello {}", claims.subject().as_str()) -} - -#[axum::debug_handler] -async fn maybe_authenticated( - claims: Result, axum_oidc::error::ExtractorError>, -) -> impl IntoResponse { - if let Ok(claims) = claims { - format!( - "Hello {}! You are already logged in from another Handler.", - claims.subject().as_str() - ) - } else { - "Hello anon!".to_string() - } -} - -async fn logout(logout: OidcRpInitiatedLogout) -> impl IntoResponse { - logout.with_post_logout_redirect(Uri::from_static("https://example.com")) -} diff --git a/examples/basic/src/main.rs b/examples/basic/src/main.rs index 0456d55..c22816d 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/src/main.rs @@ -1,9 +1,90 @@ -use basic::run; +use axum::{ + error_handling::HandleErrorLayer, + http::Uri, + response::IntoResponse, + routing::{any, get}, + Router, +}; +use axum_oidc::{ + error::MiddlewareError, handle_oidc_redirect, ClientId, ClientSecret, EmptyAdditionalClaims, + OidcAuthLayer, OidcClaims, OidcClient, OidcLoginLayer, OidcRpInitiatedLogout, +}; +use tokio::net::TcpListener; +use tower::ServiceBuilder; +use tower_sessions::{ + cookie::{time::Duration, SameSite}, + Expiry, MemoryStore, SessionManagerLayer, +}; + #[tokio::main] -async fn main() { +pub async fn run() { dotenvy::dotenv().ok(); let issuer = std::env::var("ISSUER").expect("ISSUER env variable"); let client_id = std::env::var("CLIENT_ID").expect("CLIENT_ID env variable"); let client_secret = std::env::var("CLIENT_SECRET").ok(); - run(issuer, client_id, client_secret).await + + let session_store = MemoryStore::default(); + let session_layer = SessionManagerLayer::new(session_store) + .with_secure(false) + .with_same_site(SameSite::Lax) + .with_expiry(Expiry::OnInactivity(Duration::seconds(120))); + + let oidc_login_service = ServiceBuilder::new() + .layer(HandleErrorLayer::new(|e: MiddlewareError| async { + dbg!(&e); + e.into_response() + })) + .layer(OidcLoginLayer::::new()); + + let mut oidc_client = OidcClient::::builder() + .with_default_http_client() + .with_redirect_url(Uri::from_static("http://localhost:8080/oidc")) + .with_client_id(ClientId::new(client_id)); + if let Some(client_secret) = client_secret { + oidc_client = oidc_client.with_client_secret(ClientSecret::new(client_secret)); + } + let oidc_client = oidc_client.discover(issuer).await.unwrap().build(); + + let oidc_auth_service = ServiceBuilder::new() + .layer(HandleErrorLayer::new(|e: MiddlewareError| async { + dbg!(&e); + e.into_response() + })) + .layer(OidcAuthLayer::new(oidc_client)); + + let app = Router::new() + .route("/foo", get(authenticated)) + .route("/logout", get(logout)) + .layer(oidc_login_service) + .route("/bar", get(maybe_authenticated)) + .route("/oidc", any(handle_oidc_redirect::)) + .layer(oidc_auth_service) + .layer(session_layer); + + let listener = TcpListener::bind("[::]:8080").await.unwrap(); + axum::serve(listener, app.into_make_service()) + .await + .unwrap(); +} + +async fn authenticated(claims: OidcClaims) -> impl IntoResponse { + format!("Hello {}", claims.subject().as_str()) +} + +#[axum::debug_handler] +async fn maybe_authenticated( + claims: Result, axum_oidc::error::ExtractorError>, +) -> impl IntoResponse { + if let Ok(claims) = claims { + format!( + "Hello {}! You are already logged in from another Handler.", + claims.subject().as_str() + ) + } else { + "Hello anon!".to_string() + } +} + +async fn logout(logout: OidcRpInitiatedLogout) -> impl IntoResponse { + logout.with_post_logout_redirect(Uri::from_static("https://example.com")) } diff --git a/examples/basic/tests/integration.rs b/examples/basic/tests/integration.rs deleted file mode 100644 index 8676797..0000000 --- a/examples/basic/tests/integration.rs +++ /dev/null @@ -1,94 +0,0 @@ -mod keycloak; - -use headless_chrome::Browser; -use log::info; - -use crate::keycloak::{Client, Keycloak, Realm, User}; - -#[tokio::test(flavor = "multi_thread")] -async fn first() { - env_logger::init(); - - let alice = User { - username: "alice".to_string(), - email: "alice@example.com".to_string(), - firstname: "alice".to_string(), - lastname: "doe".to_string(), - password: "alice".to_string(), - }; - - let basic_client = Client { - client_id: "axum-oidc-example-basic".to_string(), - client_secret: Some("123456".to_string()), - }; - - let keycloak = Keycloak::start(vec![Realm { - name: "test".to_string(), - users: vec![alice.clone()], - clients: vec![basic_client.clone()], - }]) - .await; - - info!("starting basic example app"); - - let app_url = "http://localhost:8080/"; - let app_handle = tokio::spawn(basic::run( - format!("{}/realms/test", keycloak.url()), - basic_client.client_id.to_string(), - basic_client.client_secret.clone(), - )); - - info!("starting browser"); - - let browser = Browser::default().unwrap(); - let tab = browser.new_tab().unwrap(); - - tab.navigate_to(&format!("{}bar", app_url)).unwrap(); - let body = tab - .wait_for_xpath(r#"/html/body/pre"#) - .unwrap() - .get_inner_text() - .unwrap(); - assert_eq!(body, "Hello anon!"); - - tab.navigate_to(&format!("{}foo", app_url)).unwrap(); - let username = tab.wait_for_xpath(r#"//*[@id="username"]"#).unwrap(); - username.type_into(&alice.username).unwrap(); - let password = tab.wait_for_xpath(r#"//*[@id="password"]"#).unwrap(); - password.type_into(&alice.password).unwrap(); - let submit = tab.wait_for_xpath(r#"//*[@id="kc-login"]"#).unwrap(); - submit.click().unwrap(); - - let body = tab - .wait_for_xpath(r#"/html/body/pre"#) - .unwrap() - .get_inner_text() - .unwrap(); - assert!(body.starts_with("Hello ") && body.contains('-')); - - tab.navigate_to(&format!("{}bar", app_url)).unwrap(); - let body = tab - .wait_for_xpath(r#"/html/body/pre"#) - .unwrap() - .get_inner_text() - .unwrap(); - assert!(body.contains("! You are already logged in from another Handler.")); - - tab.navigate_to(&format!("{}logout", app_url)).unwrap(); - tab.wait_until_navigated().unwrap(); - - tab.navigate_to(&format!("{}bar", app_url)).unwrap(); - let body = tab - .wait_for_xpath(r#"/html/body/pre"#) - .unwrap() - .get_inner_text() - .unwrap(); - assert_eq!(body, "Hello anon!"); - - tab.navigate_to(&format!("{}foo", app_url)).unwrap(); - tab.wait_until_navigated().unwrap(); - tab.find_element_by_xpath(r#"//*[@id="username"]"#).unwrap(); - - tab.close(true).unwrap(); - app_handle.abort(); -} diff --git a/examples/basic/tests/keycloak.rs b/examples/basic/tests/keycloak.rs deleted file mode 100644 index d06a5d6..0000000 --- a/examples/basic/tests/keycloak.rs +++ /dev/null @@ -1,188 +0,0 @@ -use log::info; -use std::time::Duration; -use testcontainers::runners::AsyncRunner; -use testcontainers::ContainerAsync; - -use testcontainers::core::ExecCommand; -use testcontainers::{core::WaitFor, Image, ImageExt}; - -struct KeycloakImage; - -impl Image for KeycloakImage { - fn name(&self) -> &str { - "quay.io/keycloak/keycloak" - } - - fn tag(&self) -> &str { - "latest" - } - - fn ready_conditions(&self) -> Vec { - vec![] - } -} - -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, -} - -#[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) -> Keycloak { - info!("starting keycloak"); - - let keycloak_image = KeycloakImage - .with_cmd(["start-dev".to_string()]) - .with_env_var("KEYCLOAK_ADMIN", "admin") - .with_env_var("KEYCLOAK_ADMIN_PASSWORD", "admin"); - let container = keycloak_image.start().await.unwrap(); - - let keycloak = Self { - url: format!( - "http://127.0.0.1:{}", - container.get_host_port_ipv4(8080).await.unwrap() - ), - container, - realms, - }; - - let issuer = format!( - "http://127.0.0.1:{}/realms/{}", - keycloak.container.get_host_port_ipv4(8080).await.unwrap(), - "test" - ); - - while reqwest::get(&issuer).await.is_err() { - tokio::time::sleep(Duration::from_secs(1)).await; - } - - keycloak.execute("/opt/keycloak/bin/kcadm.sh config credentials --server http://127.0.0.1:8080 --realm master --user admin --password admin".to_string()).await; - - for realm in keycloak.realms.iter() { - 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; - } - } - - keycloak - } - - pub fn url(&self) -> &str { - &self.url - } - - async fn create_realm(&self, name: &str) { - self.execute(format!( - "/opt/keycloak/bin/kcadm.sh create realms -s realm={} -s enabled=true", - name - )) - .await; - } - - async fn create_client(&self, client_id: &str, client_secret: Option<&str>, realm: &str) { - if let Some(client_secret) = client_secret { - self.execute(format!( - r#"/opt/keycloak/bin/kcadm.sh create clients -r {} -f - << EOF - {{ - "clientId": "{}", - "secret": "{}", - "redirectUris": ["*"] - }} - EOF - "#, - realm, client_id, client_secret - )) - .await; - } else { - self.execute(format!( - r#"/opt/keycloak/bin/kcadm.sh create clients -r {} -f - << EOF - {{ - "clientId": "{}", - "redirectUris": ["*"] - }} - EOF - "#, - realm, client_id - )) - .await; - } - } - - async fn create_user( - &self, - username: &str, - email: &str, - firstname: &str, - lastname: &str, - password: &str, - realm: &str, - ) { - let id = self.execute( - format!( - "/opt/keycloak/bin/kcadm.sh create users -r {} -s username={} -s enabled=true -s emailVerified=true -s email={} -s firstName={} -s lastName={}", - realm, username, email, firstname, lastname - ), - ) - .await; - self.execute(format!( - "/opt/keycloak/bin/kcadm.sh set-password -r {} --username {} --new-password {}", - realm, username, password - )) - .await; - id - } - - async fn execute(&self, cmd: String) { - let mut result = self - .container - .exec(ExecCommand::new( - ["/bin/sh", "-c", cmd.as_str()].iter().copied(), - )) - .await - .unwrap(); - // collect stdout to wait until command completion - let _output = String::from_utf8(result.stdout_to_vec().await.unwrap()); - } -}