cp-cli-platform-codechef 3.1.10

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

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

use crate::{Client, Discussion, DiscussionList, Error, UserStats, client::http_builder};

type TestResult<T = ()> = Result<T, Box<dyn std::error::Error>>;
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) -> RequestFuture<'a, T>,
) -> TestResult<(Result<T, Error>, String)> {
    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(),
        profile_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];
        loop {
            let read = stream.read(&mut buffer).await?;
            assert_ne!(read, 0, "client closed before sending its request");
            request.extend_from_slice(&buffer[..read]);
            if request.windows(4).any(|bytes| bytes == b"\r\n\r\n") {
                break;
            }
        }
        assert!(
            request
                .windows(b"\r\ncookie:".len())
                .all(|window| !window.eq_ignore_ascii_case(b"\r\ncookie:"))
        );
        stream.write_all(response.as_bytes()).await?;
        Ok::<String, Box<dyn std::error::Error>>(
            std::str::from_utf8(&request)?
                .lines()
                .next()
                .unwrap_or_default()
                .to_owned(),
        )
    };
    let (result, request) = timeout(Duration::from_secs(3), async {
        tokio::join!(operation(&client), server)
    })
    .await?;
    Ok((result, request?))
}

#[tokio::test]
async fn public_discourse_contract() -> TestResult {
    let latest = r#"{"topic_list":{"topics":[{"id":42,"title":"A title","slug":"a-title","created_at":"2026-09-22T00:00:00.000Z","last_posted_at":"2026-09-22T01:00:00.000Z","views":12,"posts_count":2,"like_count":3,"posters":[{"user_id":7}]}]},"users":[{"id":7,"username":"chef"}]}"#;
    let topic = r#"{"id":42,"title":"A title","slug":"a-title","created_at":"2026-09-22T00:00:00.000Z","last_posted_at":"2026-09-22T01:00:00.000Z","views":12,"posts_count":2,"like_count":3,"post_stream":{"posts":[{"post_number":1,"cooked":"<p>Hello <strong>world</strong></p>","username":"chef"}]}}"#;
    for (kind, body) in [("latest", latest), ("topic", topic)] {
        let response = format!(
            "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{body}",
            body.len()
        );
        let (result, request) = exchange(response, move |client| match kind {
            "latest" => Box::pin(async move {
                let list: DiscussionList = client.latest(|_, _| {}).await?;
                assert_eq!(list.discussions.len(), 1);
                assert_eq!(list.discussions[0].author.as_deref(), Some("chef"));
                assert_eq!(list.discussions[0].posts, 2);
                Ok::<_, Error>(())
            }),
            _ => Box::pin(async move {
                let discussion: Discussion = client.discussion(42, |_, _| {}).await?;
                assert_eq!(discussion.summary.slug.as_ref(), "a-title");
                assert_eq!(
                    discussion.body_html.as_ref(),
                    "<p>Hello <strong>world</strong></p>"
                );
                Ok::<_, Error>(())
            }),
        })
        .await?;
        result?;
        assert_eq!(
            request,
            if kind == "latest" {
                "GET /latest.json HTTP/1.1"
            } else {
                "GET /t/42.json HTTP/1.1"
            }
        );
    }
    Ok(())
}

#[tokio::test]
async fn public_profile_contract() -> TestResult {
    let client = Client::new()?;
    assert_eq!(
        client.profile_url("chef_7")?.as_str(),
        "https://www.codechef.com/users/chef_7"
    );
    let body = r#"<h1 class="h2-style">Chef</h1><h3>Total Problems Solved: 12</h3><div class="rating-number"> 1800 </div><small>(Highest Rating 1900)</small>No. of Contests Participated: <b>7</b>"#;
    let response = format!(
        "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{body}",
        body.len()
    );
    let (result, request) = exchange(response, |client| {
        Box::pin(async move {
            let stats: UserStats = client.user_stats("chef_7", |_, _| {}).await?;
            assert_eq!(stats.name.as_ref(), "Chef");
            assert_eq!(stats.solved, 12);
            assert_eq!(stats.rating, Some(1800));
            assert_eq!(stats.highest_rating, Some(1900));
            assert_eq!(stats.contests, Some(7));
            Ok::<_, Error>(())
        })
    })
    .await?;
    result?;
    assert_eq!(request, "GET /users/chef_7 HTTP/1.1");
    Ok(())
}