duckduckgo-core 0.1.6

DuckDuckGo search client library for duckduckgo-cli
Documentation
use tempfile::TempDir;
use time::macros::datetime;

use crate::rate_limit::RateLimitState;
use crate::rate_limit::store::{StateStore, normalize_proxy, proxy_hash};

#[test]
fn normalize_proxy_canonicalizes_default_ports_and_case() {
    assert_eq!(
        normalize_proxy("HTTP://EXAMPLE.COM:80/path/"),
        Some("http://example.com/path".to_owned())
    );
    assert_eq!(
        normalize_proxy("socks5h://Proxy.Example:1080"),
        Some("socks5h://proxy.example".to_owned())
    );
    assert_eq!(
        normalize_proxy("https://example.com:8443/a?x=1"),
        Some("https://example.com:8443/a?x=1".to_owned())
    );
}

#[test]
fn proxy_hash_is_stable_for_equivalent_proxy_urls() {
    assert_eq!(
        proxy_hash("http://example.com"),
        proxy_hash("HTTP://EXAMPLE.COM:80/")
    );
    assert_eq!(proxy_hash("not a url"), proxy_hash("not a url"));
}

#[test]
fn write_then_read_state_round_trips() {
    let dir = TempDir::new().unwrap();
    let now = datetime!(2026-05-09 00:00 UTC);
    let store = StateStore::new(dir.path().to_path_buf(), None);
    let state = RateLimitState {
        schema: 2,
        next_allowed_at: now + time::Duration::seconds(2),
        blocked_until: Some(now + time::Duration::seconds(3)),
        slowdown_until: Some(now + time::Duration::seconds(4)),
        consecutive_blocks: 2,
        last_block_reason: Some("http_202".to_owned()),
    };
    store.write_state(&state).unwrap();
    let read = store.read_state(now);
    assert_eq!(read.consecutive_blocks, 2);
    assert_eq!(read.last_block_reason.as_deref(), Some("http_202"));
    assert_eq!(read.blocked_until, state.blocked_until);
}