codoseo-core 0.0.1

Shared types for CodoSEO, the open-source SEO crawler and monitor.
Documentation
use codoseo_core::Url;
use codoseo_core::url::{normalize, url_hash};

fn base() -> Url {
    Url::parse("https://Example.com:443/a/b?x=1").unwrap()
}

fn norm(href: &str) -> Option<String> {
    normalize(&base(), href).map(|u| u.to_string())
}

#[test]
fn resolves_relative_links_and_drops_fragments() {
    assert_eq!(norm("c#frag").as_deref(), Some("https://example.com/a/c"));
}

#[test]
fn lowercases_scheme_and_host_and_drops_default_ports() {
    assert_eq!(
        norm("HTTP://EXAMPLE.com:80/Path").as_deref(),
        Some("http://example.com/Path")
    );
}

#[test]
fn keeps_query_strings_as_written() {
    assert_eq!(
        norm("/p?b=2&a=1").as_deref(),
        Some("https://example.com/p?b=2&a=1")
    );
}

#[test]
fn converts_international_hosts_to_punycode() {
    assert_eq!(
        norm("https://bücher.de/").as_deref(),
        Some("https://xn--bcher-kva.de/")
    );
}

#[test]
fn uppercases_escapes_and_decodes_unreserved_characters() {
    assert_eq!(
        norm("/a%2fb%7e?q=%7e%2f").as_deref(),
        Some("https://example.com/a%2Fb~?q=~%2F")
    );
}

#[test]
fn drops_a_trailing_dot_from_the_host() {
    assert_eq!(
        norm("https://example.com./x").as_deref(),
        Some("https://example.com/x")
    );
}

#[test]
fn rejects_non_http_links_fragments_and_blanks() {
    for bad in [
        "mailto:a@b.c",
        "tel:123",
        "javascript:void(0)",
        "data:text/html,x",
        "ftp://x/",
        "#top",
        "  ",
        "",
    ] {
        assert_eq!(norm(bad), None, "{bad:?} should be rejected");
    }
}

#[test]
fn resolves_against_a_base_href() {
    let base_href = Url::parse("https://cdn.example.com/docs/").unwrap();
    assert_eq!(
        normalize(&base_href, "page")
            .map(|u| u.to_string())
            .as_deref(),
        Some("https://cdn.example.com/docs/page")
    );
}

#[test]
fn hash_is_stable_across_equivalent_spellings() {
    let a = Url::parse("https://example.com/a").unwrap();
    let b = normalize(&base(), "/a#x").unwrap();
    assert_eq!(url_hash(&a), url_hash(&b));
    assert_ne!(
        url_hash(&a),
        url_hash(&Url::parse("https://example.com/b").unwrap())
    );
}

#[test]
fn drops_an_empty_query() {
    assert_eq!(norm("/a?").as_deref(), Some("https://example.com/a"));
    assert_eq!(norm("/a?x").as_deref(), Some("https://example.com/a?x"));
}