duckduckgo-core 0.1.6

DuckDuckGo search client library for duckduckgo-cli
Documentation
use std::sync::Arc;
use std::time::Duration;

use tempfile::TempDir;
use time::macros::datetime;

use crate::ManualClock;
use crate::parser::BlockReason;
use crate::rate_limit::config::Limits;
use crate::rate_limit::wait::WaitTracker;
use crate::rate_limit::{AttemptOutcome, RateLimitWait, RateLimiter};

fn limiter(dir: &TempDir) -> RateLimiter {
    RateLimiter::new(
        dir.path().to_path_buf(),
        None,
        Limits::test_fast(100, 200, 2),
        Arc::new(ManualClock::new(datetime!(2026-05-09 00:00 UTC))),
    )
}

#[test]
fn block_ratchets_slowdown_until_forward() {
    let dir = TempDir::new().unwrap();
    let limiter = limiter(&dir);
    let first = datetime!(2026-05-09 00:00 UTC);
    let second = first + time::Duration::seconds(5);
    limiter
        .update_post_flight(AttemptOutcome::Block(BlockReason::Http202), first)
        .unwrap();
    let original = limiter.store.read_state(first).slowdown_until.unwrap();
    limiter
        .update_post_flight(AttemptOutcome::Block(BlockReason::Http429), second)
        .unwrap();
    let updated = limiter.store.read_state(second).slowdown_until.unwrap();
    assert!(updated > original);
}

#[test]
fn success_clears_block_metadata() {
    let dir = TempDir::new().unwrap();
    let limiter = limiter(&dir);
    let now = datetime!(2026-05-09 00:00 UTC);
    limiter
        .update_post_flight(AttemptOutcome::Block(BlockReason::Http202), now)
        .unwrap();
    limiter
        .update_post_flight(AttemptOutcome::Success, now + time::Duration::seconds(3))
        .unwrap();
    let state = limiter.store.read_state(now + time::Duration::seconds(3));
    assert_eq!(state.consecutive_blocks, 0);
    assert!(state.blocked_until.is_none());
    assert!(state.last_block_reason.is_none());
}

#[tokio::test(flavor = "current_thread")]
async fn wait_or_abort_no_wait_returns_blocked_without_sleep() {
    let dir = TempDir::new().unwrap();
    let limiter = limiter(&dir);
    let err = limiter
        .wait_or_abort(
            RateLimitWait::Spacing,
            time::Duration::seconds(1),
            true,
            &mut WaitTracker::new(),
            0,
        )
        .await
        .unwrap_err();
    assert!(err.to_string().contains("spacing wait required"));
}

#[test]
fn other_outcome_does_not_change_block_state() {
    let dir = TempDir::new().unwrap();
    let limiter = limiter(&dir);
    let now = datetime!(2026-05-09 00:00 UTC);
    limiter
        .update_post_flight(AttemptOutcome::Other, now)
        .unwrap();
    assert_eq!(limiter.store.read_state(now).consecutive_blocks, 0);
}

#[test]
fn cooldown_uses_configured_limits() {
    assert_eq!(
        Limits::test_fast(100, 200, 2).cooldown_for(2),
        Duration::from_secs(4)
    );
}