diff --git a/Cargo.lock b/Cargo.lock index 87a87ea..73ae835 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3319,6 +3319,7 @@ dependencies = [ "caldata", "clap", "figment", + "futures-util", "headers", "http", "insta", @@ -3344,6 +3345,7 @@ dependencies = [ "similar-asserts", "sqlx", "tokio", + "tokio-util", "toml 0.9.11+spec-1.1.0", "tower", "tower-http", @@ -4471,6 +4473,7 @@ dependencies = [ "bytes", "futures-core", "futures-sink", + "futures-util", "pin-project-lite", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index abcf2e0..cfb4155 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ tokio = { version = "1.48", features = [ "rt-multi-thread", "full", ] } +tokio-util = { version = "0.7", features = ["rt"] } url = "2.5" base64 = "0.22" thiserror = "2.0" @@ -166,6 +167,7 @@ caldata.workspace = true toml.workspace = true serde.workspace = true tokio.workspace = true +tokio-util.workspace = true tracing.workspace = true anyhow.workspace = true clap.workspace = true @@ -204,3 +206,4 @@ tower-http.workspace = true axum-extra.workspace = true headers.workspace = true http.workspace = true +futures-util.workspace = true diff --git a/tests/common/mod.rs b/tests/common/mod.rs index e69de29..598b3a8 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -0,0 +1,72 @@ +use rustical::{ + Args, cmd_default, + config::{Config, DataStoreConfig, HttpConfig, SqliteDataStoreConfig}, +}; +use std::{ + net::{Ipv4Addr, SocketAddrV4, TcpListener}, + sync::Arc, + thread::{self, JoinHandle}, +}; +use tokio::sync::Notify; +use tokio_util::sync::CancellationToken; + +pub fn find_free_port() -> Option { + let mut port = 15000; + // Frees the socket on drop such that this function returns a free port + while TcpListener::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port)).is_err() { + port += 1; + + if port >= 16000 { + return None; + } + } + Some(port) +} + +pub fn rustical_process() -> (CancellationToken, u16, JoinHandle<()>, Arc) { + let port = find_free_port().unwrap(); + let token = CancellationToken::new(); + let cloned_token = token.clone(); + let start_notify = Arc::new(Notify::new()); + let cloned_start_notify = start_notify.clone(); + + let main_process = thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let fut = async { + cmd_default( + Args { + config_file: "asldajldakjsdkj".to_owned(), + no_migrations: false, + command: None, + }, + Config { + data_store: DataStoreConfig::Sqlite(SqliteDataStoreConfig { + db_url: ":memory:".to_owned(), + run_repairs: true, + skip_broken: false, + }), + http: HttpConfig { + host: "127.0.0.1".to_owned(), + port, + ..Default::default() + }, + frontend: Default::default(), + oidc: None, + tracing: Default::default(), + dav_push: Default::default(), + nextcloud_login: Default::default(), + caldav: Default::default(), + }, + Some(cloned_start_notify), + ) + .await + }; + rt.block_on(async { + tokio::select! { + _ = cloned_token.cancelled() => {}, + _ = fut => {} + } + }); + }); + (token, port, main_process, start_notify) +} diff --git a/tests/http_integration.rs b/tests/http_integration.rs new file mode 100644 index 0000000..dfe9742 --- /dev/null +++ b/tests/http_integration.rs @@ -0,0 +1,44 @@ +// This integration test checks whether the HTTP server works by actually running rustical in a new +// thread. +use common::rustical_process; +use http::StatusCode; +use std::time::Duration; + +mod common; + +pub async fn test_runner(inner: F) +where + O: IntoFuture, + // ::IntoFuture: UnwindSafe, + F: FnOnce(String) -> O, +{ + // Start RustiCal process + let (token, port, main_process, start_notify) = rustical_process(); + let origin = format!("http://localhost:{port}"); + + // Wait for RustiCal server to listen + tokio::time::timeout(Duration::new(2, 0), start_notify.notified()) + .await + .unwrap(); + + // We use catch_unwind to make sure we'll always correctly stop RustiCal + // Otherwise, our process would just run indefinitely + inner(origin).into_future().await; + + // Signal RustiCal to stop + token.cancel(); + main_process.join().unwrap(); +} + +#[tokio::test] +async fn test_ping() { + test_runner(async |origin| { + let resp = reqwest::get(origin.clone() + "/ping").await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + + // Ensure that path normalisation works as intended + let resp = reqwest::get(origin + "/ping/").await.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + }) + .await +} diff --git a/tests/light_integrations.rs b/tests/light_integrations.rs deleted file mode 100644 index 0721481..0000000 --- a/tests/light_integrations.rs +++ /dev/null @@ -1 +0,0 @@ -mod integration_tests; diff --git a/tests/run_integration_tests.rs b/tests/run_integration_tests.rs new file mode 100644 index 0000000..4e2f118 --- /dev/null +++ b/tests/run_integration_tests.rs @@ -0,0 +1,3 @@ +// This file is just an entrypoint for integration_tests +// since the test runner only looks for files to run. +mod integration_tests;