tovuk 0.1.114

Use Tovuk scraper APIs from a native CLI.
use std::{
    error::Error,
    io::{Read, Write},
    net::TcpListener,
    thread::{self, JoinHandle},
};

use reqwest::Method;

use crate::cli::args::CliOptions;

use super::api_request;

type TestServer = JoinHandle<Result<(), String>>;

#[test]
fn api_unreachable_points_agents_to_status_docs() -> Result<(), Box<dyn Error>> {
    let listener = TcpListener::bind("127.0.0.1:0")?;
    let api_url = format!("http://{}", listener.local_addr()?);
    drop(listener);
    let cli = CliOptions {
        api_url,
        ..CliOptions::default()
    };

    let error = match api_request(&cli, Method::GET, "/v1/status", None, None) {
        Ok(_response) => return Err("request unexpectedly succeeded".into()),
        Err(error) => error,
    };
    let payload = error.payload();

    if payload.code != "api_unreachable" {
        return Err(format!("unexpected code: {}", payload.code).into());
    }
    if payload.docs_url.as_deref() != Some("https://docs.tovuk.com/status") {
        return Err(format!("unexpected docs url: {:?}", payload.docs_url).into());
    }
    Ok(())
}

#[test]
fn successful_api_response_must_be_valid_json() -> Result<(), Box<dyn Error>> {
    let (api_url, server) = serve_once("200 OK", "not-json")?;
    let cli = CliOptions {
        api_url,
        ..CliOptions::default()
    };

    let error = match api_request(&cli, Method::GET, "/v1/status", None, None) {
        Ok(_response) => return Err("request unexpectedly succeeded".into()),
        Err(error) => error,
    };
    join_server(server)?;
    let payload = error.payload();

    if payload.code != "internal_error" {
        return Err(format!("unexpected code: {}", payload.code).into());
    }
    if !payload.message.contains("invalid JSON") {
        return Err(format!("unexpected message: {}", payload.message).into());
    }
    Ok(())
}

#[test]
fn malformed_error_response_still_reports_http_status() -> Result<(), Box<dyn Error>> {
    let (api_url, server) = serve_once("503 Service Unavailable", "<html>down</html>")?;
    let cli = CliOptions {
        api_url,
        ..CliOptions::default()
    };

    let error = match api_request(&cli, Method::GET, "/v1/status", None, None) {
        Ok(_response) => return Err("request unexpectedly succeeded".into()),
        Err(error) => error,
    };
    join_server(server)?;
    let payload = error.payload();

    if payload.code != "api_error" {
        return Err(format!("unexpected code: {}", payload.code).into());
    }
    if payload.message != "Tovuk API returned HTTP 503." {
        return Err(format!("unexpected message: {}", payload.message).into());
    }
    Ok(())
}

fn serve_once(status: &str, body: &str) -> Result<(String, TestServer), Box<dyn Error>> {
    let listener = TcpListener::bind("127.0.0.1:0")?;
    let api_url = format!("http://{}", listener.local_addr()?);
    let status = status.to_owned();
    let body = body.to_owned();
    let server = thread::spawn(move || {
        let (mut stream, _) = listener.accept().map_err(|error| error.to_string())?;
        let mut request = [0_u8; 1024];
        let _request_size = stream
            .read(&mut request)
            .map_err(|error| error.to_string())?;
        let response = format!(
            "HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
            body.len()
        );
        stream
            .write_all(response.as_bytes())
            .map_err(|error| error.to_string())?;
        Ok(())
    });
    Ok((api_url, server))
}

fn join_server(server: TestServer) -> Result<(), Box<dyn Error>> {
    match server.join() {
        Ok(Ok(())) => Ok(()),
        Ok(Err(error)) => Err(error.into()),
        Err(_) => Err("test server panicked".into()),
    }
}