cp-cli-platform-hackerrank 3.1.10

HackerRank HTTP client used by cp-cli
Documentation
use std::{future::Future, io, pin::Pin, time::Duration};

use serde_json::json;
use tokio::{
    io::{AsyncReadExt, AsyncWriteExt},
    net::TcpListener,
    time::timeout,
};

use crate::{Client, Difficulty, Error, Problem, ProblemList, Profile, client::http_builder};

type TestResult<T = ()> = Result<T, Box<dyn std::error::Error>>;
type Progress = Vec<(usize, Option<u64>)>;
type RequestFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, Error>> + 'a>>;

async fn exchange<T>(
    response: String,
    operation: impl for<'a> FnOnce(&'a Client, &'a mut Progress) -> RequestFuture<'a, T>,
) -> TestResult<(Result<T, Error>, String, Progress)> {
    let listener = TcpListener::bind("127.0.0.1:0").await?;
    let client = Client {
        http: http_builder()
            .https_only(false)
            .no_proxy()
            .timeout(Duration::from_secs(2))
            .build()?,
        endpoint: format!("http://{}/", listener.local_addr()?).into(),
    };
    let server = async {
        let (mut stream, _) = listener.accept().await?;
        let mut request = Vec::new();
        let mut buffer = [0; 2048];
        let end = loop {
            let read = stream.read(&mut buffer).await?;
            assert_ne!(read, 0, "client closed before sending its request");
            request.extend_from_slice(&buffer[..read]);
            assert!(request.len() < 16 * 1024);
            if let Some(end) = request.windows(4).position(|bytes| bytes == b"\r\n\r\n") {
                break end + 4;
            }
        };
        let headers = std::str::from_utf8(&request[..end - 4])?;
        let user_agent = format!("user-agent: {}/", env!("CARGO_PKG_NAME"));
        assert!(headers.starts_with("GET /rest/contests/master/"));
        assert!(
            headers
                .to_ascii_lowercase()
                .lines()
                .any(|line| line.starts_with(&user_agent))
        );
        assert!(!headers.to_ascii_lowercase().contains("\r\ncookie:"));
        if let Err(error) = stream.write_all(response.as_bytes()).await
            && !matches!(
                error.kind(),
                io::ErrorKind::BrokenPipe | io::ErrorKind::ConnectionReset
            )
        {
            return Err(error.into());
        }
        Ok::<String, Box<dyn std::error::Error>>(
            headers.lines().next().unwrap_or_default().to_owned(),
        )
    };
    let mut progress = Vec::new();
    let (result, request) = timeout(Duration::from_secs(3), async {
        tokio::join!(operation(&client, &mut progress), server)
    })
    .await?;
    Ok((result, request?, progress))
}

#[tokio::test]
async fn public_problem_contract() -> TestResult {
    let list = json!({"models": [{
        "id": 2532, "slug": "solve-me-first", "name": "Solve Me First",
        "difficulty_name": "Easy", "preview": "Add two integers",
        "track": {"slug": "warmup", "name": "Warmup", "track_slug": "algorithms", "track_name": "Algorithms"}
    }], "total": 10})
    .to_string();
    let show = json!({"status": true, "model": {
        "id": 2532, "slug": "solve-me-first", "name": "Solve Me First",
        "difficulty_name": "Easy", "preview": "Add two integers",
        "track": {"slug": "warmup", "name": "Warmup", "track_slug": "algorithms", "track_name": "Algorithms"},
        "problem_statement": "Compute $a + b$", "input_format": "Two integers", "output_format": "Their sum",
        "languages": ["cpp", "python3", "rust"],
        "onboarding": {
            "cpp": {"template": "int main() {}"},
            "statement": "Ignored metadata"
        },
"sample_test_cases": [{"input": "2\n7", "output": "9"}],
        "public_test_cases": true
    }})
    .to_string();
    let profile = json!({"model": {
        "id": 15795, "username": "hARRY", "name": "harry", "country": "India",
        "level": 4, "event_count": 7, "created_at": "2012-10-14T07:00:40.000Z"
    }})
    .to_string();

    for (kind, body) in [("list", list), ("show", show), ("profile", profile)] {
        let length = body.len();
        let response = format!("HTTP/1.1 200 OK\r\nContent-Length: {length}\r\n\r\n{body}");
        let (result, request, progress) = exchange(response, move |client, progress| match kind {
            "list" => Box::pin(async move {
                let list: ProblemList = client
                    .list_track("algorithms", 0, 20, |bytes, total| {
                        progress.push((bytes, total))
                    })
                    .await?;
                assert_eq!(list.total, 10);
                assert_eq!(list.problems[0].slug.as_ref(), "solve-me-first");
                assert_eq!(list.problems[0].difficulty, Difficulty::Easy);
                Ok::<_, Error>(())
            }),
            _ => Box::pin(async move {
                if kind == "profile" {
                    let profile: Profile = client
                        .profile("hARRY", |bytes, total| progress.push((bytes, total)))
                        .await?;
                    assert_eq!(profile.username.as_ref(), "hARRY");
                    assert_eq!(profile.level, Some(4));
                    assert_eq!(profile.event_count, Some(7));
                    return Ok::<_, Error>(());
                }
                let problem: Problem = client
                    .problem("solve-me-first", |bytes, total| {
                        progress.push((bytes, total))
                    })
                    .await?;
                assert_eq!(problem.statement.as_ref(), "Compute $a + b$");
                assert_eq!(problem.starters.len(), 1);
                assert_eq!(problem.starters[0].language.as_ref(), "cpp");
                assert_eq!(problem.starters[0].template.as_ref(), "int main() {}");
                assert_eq!(problem.samples[0].input.as_ref(), "2\n7");
                assert_eq!(problem.samples[0].output.as_ref(), "9");
                assert!(problem.has_public_test_cases);
                assert_eq!(
                    problem
                        .languages
                        .iter()
                        .map(|language| language.as_ref())
                        .collect::<Vec<_>>(),
                    ["cpp", "python3", "rust"]
                );
                Ok::<_, Error>(())
            }),
        })
        .await?;
        result?;
        match kind {
            "list" => assert_eq!(
                request,
                "GET /rest/contests/master/tracks/algorithms/challenges?offset=0&limit=20&track_login=true HTTP/1.1"
            ),
            "show" => assert_eq!(
                request,
                "GET /rest/contests/master/challenges/solve-me-first HTTP/1.1"
            ),
            _ => assert_eq!(
                request,
                "GET /rest/contests/master/hackers/hARRY/profile HTTP/1.1"
            ),
        }
        assert_eq!(progress.first(), Some(&(0, Some(length as u64))));
        assert_eq!(progress.last(), Some(&(length, Some(length as u64))));
    }
    Ok(())
}