rust-web-server 17.45.0

An HTTP web framework, reverse proxy, and server for Rust supporting HTTP/1.1, HTTP/2, and HTTP/3. Config-driven proxy mode (rws.config.toml with [[route]] / [[upstream]]) or library crate. No third-party HTTP dependencies.
Documentation
use std::thread;
use std::time::Duration;

use crate::header::Header;
use crate::http::VERSION;
use crate::request::{METHOD, Request};
use crate::session::{self, SessionStore};

fn empty_get() -> Request {
    Request {
        method: METHOD.get.to_string(),
        request_uri: "/".to_string(),
        http_version: VERSION.http_1_1.to_string(),
        headers: vec![],
        body: vec![],
    }
}

fn get_with_cookie(cookie_header: &str) -> Request {
    Request {
        method: METHOD.get.to_string(),
        request_uri: "/".to_string(),
        http_version: VERSION.http_1_1.to_string(),
        headers: vec![Header {
            name: "Cookie".to_string(),
            value: cookie_header.to_string(),
        }],
        body: vec![],
    }
}

// ── Session get / set / remove / contains ────────────────────────────────────

#[test]
fn session_get_returns_none_for_missing_key() {
    let store = SessionStore::new(3600);
    let session = store.create();
    assert!(session.get("missing").is_none());
}

#[test]
fn session_set_and_get() {
    let store = SessionStore::new(3600);
    let mut session = store.create();
    session.set("user_id", "42");
    assert_eq!(Some("42"), session.get("user_id"));
}

#[test]
fn session_contains() {
    let store = SessionStore::new(3600);
    let mut session = store.create();
    assert!(!session.contains("k"));
    session.set("k", "v");
    assert!(session.contains("k"));
}

#[test]
fn session_remove() {
    let store = SessionStore::new(3600);
    let mut session = store.create();
    session.set("x", "1");
    session.remove("x");
    assert!(session.get("x").is_none());
}

// ── SessionStore create / load / save ────────────────────────────────────────

#[test]
fn create_generates_non_empty_id() {
    let store = SessionStore::new(3600);
    let session = store.create();
    assert!(!session.id.is_empty());
}

#[test]
fn create_with_id_uses_provided_id() {
    let store = SessionStore::new(3600);
    let session = store.create_with_id("my-custom-id".to_string());
    assert_eq!("my-custom-id", session.id);
}

#[test]
fn load_returns_created_session() {
    let store = SessionStore::new(3600);
    let session = store.create();
    let id = session.id.clone();
    let loaded = store.load(&id);
    assert!(loaded.is_some());
    assert_eq!(id, loaded.unwrap().id);
}

#[test]
fn load_unknown_id_returns_none() {
    let store = SessionStore::new(3600);
    assert!(store.load("no-such-id").is_none());
}

#[test]
fn save_persists_changes() {
    let store = SessionStore::new(3600);
    let mut session = store.create();
    let id = session.id.clone();
    session.set("role", "admin");
    store.save(&session);

    let loaded = store.load(&id).unwrap();
    assert_eq!(Some("admin"), loaded.get("role"));
}

#[test]
fn unsaved_changes_are_not_visible() {
    let store = SessionStore::new(3600);
    let mut session = store.create();
    let id = session.id.clone();
    session.set("role", "admin");
    // no save()
    let loaded = store.load(&id).unwrap();
    assert!(loaded.get("role").is_none());
}

// ── destroy ───────────────────────────────────────────────────────────────────

#[test]
fn destroy_removes_session() {
    let store = SessionStore::new(3600);
    let session = store.create();
    let id = session.id.clone();
    store.destroy(&id);
    assert!(store.load(&id).is_none());
}

// ── expiry and purge ──────────────────────────────────────────────────────────

#[test]
fn expired_session_not_loadable() {
    let store = SessionStore::new(0); // 0-second TTL — expires immediately
    let session = store.create();
    let id = session.id.clone();
    thread::sleep(Duration::from_millis(5));
    assert!(store.load(&id).is_none());
}

#[test]
fn purge_expired_removes_expired_entries() {
    let store = SessionStore::new(0);
    store.create();
    store.create();
    assert_eq!(2, store.len());
    thread::sleep(Duration::from_millis(5));
    store.purge_expired();
    assert_eq!(0, store.len());
}

#[test]
fn purge_expired_keeps_live_sessions() {
    let store = SessionStore::new(3600);
    store.create();
    store.create();
    store.purge_expired();
    assert_eq!(2, store.len());
}

#[test]
fn is_empty_reflects_store_state() {
    let store = SessionStore::new(3600);
    assert!(store.is_empty());
    let s = store.create();
    assert!(!store.is_empty());
    store.destroy(&s.id);
    assert!(store.is_empty());
}

// ── clone shares backing store ────────────────────────────────────────────────

#[test]
fn cloned_store_shares_data() {
    let store = SessionStore::new(3600);
    let clone = store.clone();
    let mut session = store.create();
    session.set("k", "v");
    store.save(&session);

    let loaded = clone.load(&session.id).unwrap();
    assert_eq!(Some("v"), loaded.get("k"));
}

// ── cookie helpers ────────────────────────────────────────────────────────────

#[test]
fn session_id_from_request_reads_named_cookie() {
    let req = get_with_cookie("sid=abc123; other=xyz");
    let id = session::session_id_from_request(&req, "sid");
    assert_eq!(Some("abc123".to_string()), id);
}

#[test]
fn session_id_from_request_returns_none_when_missing() {
    let req = get_with_cookie("other=xyz");
    assert!(session::session_id_from_request(&req, "sid").is_none());
}

#[test]
fn session_id_from_request_returns_none_without_cookie_header() {
    let req = empty_get();
    assert!(session::session_id_from_request(&req, "sid").is_none());
}

#[test]
fn session_cookie_contains_id_and_name() {
    let value = session::session_cookie("tok123", "sid", 3600);
    assert!(value.contains("sid=tok123"), "got: {}", value);
    assert!(value.contains("Max-Age=3600"), "got: {}", value);
    assert!(value.contains("HttpOnly"), "got: {}", value);
    assert!(value.contains("SameSite=Lax"), "got: {}", value);
}

#[test]
fn destroy_cookie_sets_max_age_zero() {
    let value = session::destroy_cookie("sid");
    assert!(value.contains("sid="), "got: {}", value);
    assert!(value.contains("Max-Age=0"), "got: {}", value);
}

// ── DbSessionStore ────────────────────────────────────────────────────────────

#[cfg(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql"))]
mod db_session_tests {
    use crate::model::DbPool;
    use crate::session::DbSessionStore;

    async fn make_store() -> DbSessionStore {
        let pool = DbPool::memory().await.expect("in-memory pool");
        DbSessionStore::new(pool, 3600).await.expect("create store")
    }

    #[tokio::test]
    async fn db_create_and_load_session() {
        let store = make_store().await;
        let sess = store.create().await.unwrap();
        let loaded = store.load(&sess.id).await.unwrap();
        assert!(loaded.is_some());
        assert_eq!(sess.id, loaded.unwrap().id);
    }

    #[tokio::test]
    async fn db_load_unknown_id_returns_none() {
        let store = make_store().await;
        assert!(store.load("no-such-id").await.unwrap().is_none());
    }

    #[tokio::test]
    async fn db_save_persists_data() {
        let store = make_store().await;
        let mut sess = store.create().await.unwrap();
        sess.set("role", "admin");
        store.save(&sess).await.unwrap();
        let loaded = store.load(&sess.id).await.unwrap().unwrap();
        assert_eq!(Some("admin"), loaded.get("role"));
    }

    #[tokio::test]
    async fn db_unsaved_changes_not_visible() {
        let store = make_store().await;
        let mut sess = store.create().await.unwrap();
        sess.set("role", "admin");
        // no save()
        let loaded = store.load(&sess.id).await.unwrap().unwrap();
        assert!(loaded.get("role").is_none());
    }

    #[tokio::test]
    async fn db_destroy_removes_session() {
        let store = make_store().await;
        let sess = store.create().await.unwrap();
        let id = sess.id.clone();
        store.destroy(&id).await.unwrap();
        assert!(store.load(&id).await.unwrap().is_none());
    }

    #[tokio::test]
    async fn db_purge_expired_removes_stale_rows() {
        let pool = DbPool::memory().await.expect("in-memory pool");
        let store = DbSessionStore::new(pool, 0).await.expect("create store");
        store.create().await.unwrap();
        store.create().await.unwrap();
        assert_eq!(2, store.len().await.unwrap());
        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
        store.purge_expired().await.unwrap();
        assert_eq!(0, store.len().await.unwrap());
    }

    #[tokio::test]
    async fn db_len_and_is_empty() {
        let store = make_store().await;
        assert!(store.is_empty().await.unwrap());
        store.create().await.unwrap();
        assert_eq!(1, store.len().await.unwrap());
        assert!(!store.is_empty().await.unwrap());
    }

    #[tokio::test]
    async fn db_expired_session_not_loadable() {
        let pool = DbPool::memory().await.expect("in-memory pool");
        let store = DbSessionStore::new(pool, 0).await.expect("create store");
        let sess = store.create().await.unwrap();
        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
        assert!(store.load(&sess.id).await.unwrap().is_none());
    }

    #[tokio::test]
    async fn db_clone_shares_pool() {
        let store = make_store().await;
        let clone = store.clone();
        let mut sess = store.create().await.unwrap();
        sess.set("x", "1");
        store.save(&sess).await.unwrap();
        let loaded = clone.load(&sess.id).await.unwrap().unwrap();
        assert_eq!(Some("1"), loaded.get("x"));
    }

    #[tokio::test]
    async fn db_session_data_with_special_chars() {
        let store = make_store().await;
        let mut sess = store.create().await.unwrap();
        sess.set("name", "O'Brien & Co");
        sess.set("redirect", "/foo?bar=baz");
        store.save(&sess).await.unwrap();
        let loaded = store.load(&sess.id).await.unwrap().unwrap();
        assert_eq!(Some("O'Brien & Co"), loaded.get("name"));
        assert_eq!(Some("/foo?bar=baz"), loaded.get("redirect"));
    }
}