use axum::body::Body;
use axum::http::{header, Request, StatusCode};
use axum::Router;
use larust_core::Application;
use larust_http::session::Session;
use tower::ServiceExt;
fn file_driver_config() -> serde_json::Value {
serde_json::json!({ "session_driver": "file" })
}
fn shared_app_root() -> std::path::PathBuf {
static ROOT: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
ROOT.get_or_init(|| {
let dir = tempfile::tempdir().unwrap().keep();
Application::at_root(&dir, file_driver_config).unwrap();
dir
})
.clone()
}
async fn shared_pool() -> sqlx::AnyPool {
let dir = tempfile::tempdir().unwrap().keep();
let database_url = format!("sqlite://{}/test.sqlite", dir.display());
let _ = larust_orm::connect(&database_url).await;
larust_orm::pool().unwrap().clone()
}
async fn greet(session: Session) -> String {
session
.insert("greeting", "hi")
.await
.expect("session insert must succeed");
"greeted".to_string()
}
async fn recall(session: Session) -> String {
session
.get::<String>("greeting")
.await
.expect("session get must succeed")
.unwrap_or_default()
}
async fn app() -> Router {
let _ = shared_app_root();
let pool = shared_pool().await;
larust_http::Route::get("/greet", greet)
.get("/recall", recall)
.with_sessions(&pool, true)
.await
.unwrap()
.into_axum_router()
}
#[tokio::test]
async fn session_driver_file_actually_uses_the_file_backend_not_the_database() {
let root = shared_app_root();
let router = app().await;
let response = router
.oneshot(Request::get("/greet").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let cookie = response
.headers()
.get(header::SET_COOKIE)
.expect("session cookie should be set")
.to_str()
.unwrap()
.split(';')
.next()
.unwrap()
.to_string();
let session_id = cookie.split('=').nth(1).expect("cookie must have a value");
let session_file = root.join("storage/sessions").join(session_id);
assert!(
session_file.exists(),
"expected a session file at {}, but it doesn't exist - SESSION_DRIVER=file did not \
actually route through FileSessionStore",
session_file.display()
);
let contents = std::fs::read_to_string(&session_file).unwrap();
assert!(
contents.contains("greeting") && contents.contains("hi"),
"session file content should contain the saved key/value, got: {contents}"
);
}
#[tokio::test]
async fn session_driver_file_round_trips_data_across_two_separate_requests() {
let router = app().await;
let greet_response = router
.clone()
.oneshot(Request::get("/greet").body(Body::empty()).unwrap())
.await
.unwrap();
let cookie = greet_response
.headers()
.get(header::SET_COOKIE)
.expect("session cookie should be set")
.to_str()
.unwrap()
.split(';')
.next()
.unwrap()
.to_string();
let recall_response = router
.oneshot(
Request::get("/recall")
.header(header::COOKIE, &cookie)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(recall_response.status(), StatusCode::OK);
let body = axum::body::to_bytes(recall_response.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(String::from_utf8(body.to_vec()).unwrap(), "hi");
}
#[tokio::test]
async fn a_session_saved_by_one_store_handle_is_readable_by_a_fresh_one_over_the_same_directory() {
use larust_http::session::FileSessionStore;
use tower_sessions::session::Record;
use tower_sessions::SessionStore;
let root = shared_app_root();
let dir = root.join("storage/sessions");
tokio::fs::create_dir_all(&dir).await.unwrap();
let first_handle = FileSessionStore::new(dir.clone());
let mut record = Record {
id: Default::default(),
data: Default::default(),
expiry_date: time::OffsetDateTime::now_utc() + time::Duration::hours(1),
};
record
.data
.insert("survives".to_string(), serde_json::json!("a restart"));
first_handle.create(&mut record).await.unwrap();
let second_handle = FileSessionStore::new(dir);
let loaded = second_handle
.load(&record.id)
.await
.unwrap()
.expect("a fresh store handle must still see the other handle's saved session");
assert_eq!(loaded.data["survives"], serde_json::json!("a restart"));
}