leyline-http 0.1.1

HTTP client that mimics browsers on the wire
Documentation
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use url::Url;

use crate::cookie::Jar;
use crate::util::lock;

fn url() -> Url {
    Url::parse("https://www.example.com/").unwrap()
}

fn millis(at: SystemTime) -> i64 {
    at.duration_since(UNIX_EPOCH).unwrap().as_millis() as i64
}

fn saved(name: &str, value: &str, expires: Option<i64>, created: i64) -> serde_json::Value {
    serde_json::json!({
        "name": name,
        "value": value,
        "domain": "www.example.com",
        "path": "/",
        "secure": true,
        "http_only": false,
        "same_site": "Lax",
        "expires": expires,
        "creation_time": created,
        "host_only": true,
    })
}

#[test]
fn changes_fire_on_writes_through_any_clone_and_not_on_reads() {
    let jar = Jar::new();
    let mut changes = jar.changes();

    jar.clone()
        .store_set_cookie("session-token=a; Path=/", &url());
    assert!(changes.has_changed().unwrap());
    changes.mark_unchanged();

    assert!(jar.cookie_header(&url()).is_some());
    assert!(!changes.has_changed().unwrap());

    jar.remove_named("session-token");
    assert!(changes.has_changed().unwrap());
}

#[test]
fn load_drops_expired_cookies_and_keeps_the_newest_duplicate() {
    let now = millis(SystemTime::now());
    let saved = serde_json::Value::Array(vec![
        saved("gone", "1", Some(now - 60_000), now - 120_000),
        saved("token", "old", None, now - 60_000),
        saved("token", "new", None, now - 1_000),
    ]);
    let jar: Jar = serde_json::from_value(saved).unwrap();
    let cookies = jar.all_cookies();
    assert_eq!(cookies.len(), 1);
    assert_eq!(cookies[0].name, "token");
    assert_eq!(cookies[0].value, "new");
}

#[test]
fn save_drops_cookies_that_expired_in_memory() {
    let jar = Jar::new();
    jar.store_set_cookie("brief=1; Path=/; Max-Age=3600", &url());
    jar.store_set_cookie("kept=1; Path=/; Max-Age=3600", &url());
    for cookie in lock(&jar.inner).cookies.values_mut().flatten() {
        if cookie.name == "brief" {
            cookie.expires = Some(SystemTime::now() - Duration::from_secs(1));
        }
    }
    let saved = serde_json::to_value(&jar).unwrap();
    let names: Vec<&str> = saved
        .as_array()
        .unwrap()
        .iter()
        .map(|c| c["name"].as_str().unwrap())
        .collect();
    assert_eq!(names, ["kept"]);
}

#[test]
fn last_access_survives_a_round_trip() {
    let jar = Jar::new();
    jar.store_set_cookie("token=a; Path=/", &url());
    jar.cookie_header(&url());
    let accessed = millis(jar.all_cookies()[0].last_access);

    let restored: Jar = serde_json::from_str(&serde_json::to_string(&jar).unwrap()).unwrap();
    assert_eq!(millis(restored.all_cookies()[0].last_access), accessed);
}

#[test]
fn jar_saved_without_last_access_still_loads() {
    let now = millis(SystemTime::now());
    let jar: Jar = serde_json::from_value(serde_json::Value::Array(vec![saved(
        "token", "a", None, now,
    )]))
    .unwrap();
    assert_eq!(jar.get_cookie(&url(), "token").as_deref(), Some("a"));
}