Add more abstract integration test

This commit is contained in:
Lennart K
2026-01-28 20:54:55 +01:00
parent c1758e2cba
commit 494f31f992
6 changed files with 125 additions and 1 deletions

View File

@@ -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<u16> {
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<Notify>) {
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)
}

44
tests/http_integration.rs Normal file
View File

@@ -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<O, F>(inner: F)
where
O: IntoFuture<Output = ()>,
// <O as 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
}

View File

@@ -1 +0,0 @@
mod integration_tests;

View File

@@ -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;