ureq-proto 0.7.1

ureq support crate
Documentation
use http::{Response, StatusCode};

use crate::Error;

use super::scenario::Scenario;

#[test]
fn provide_successful_response() {
    // Create a scenario with a GET request
    let scenario = Scenario::builder().get("/path").build();

    // Get a Reply in the ProvideResponse state
    let reply = scenario.to_provide_response();

    // Create a successful response
    let response = Response::builder()
        .status(StatusCode::OK)
        .header("content-type", "text/plain")
        .body(())
        .unwrap();

    // Provide the response
    let reply = reply.provide(response).unwrap();

    // Verify the state transition
    assert!(reply.inner().response.is_some());
    assert!(reply.inner().state.writer.is_some());

    // Verify the status code
    let status = reply.inner().response.as_ref().unwrap().prelude().1;
    assert_eq!(status, StatusCode::OK);
}

#[test]
fn provide_error_response() {
    // Create a scenario with a GET request
    let scenario = Scenario::builder().get("/path").build();

    // Get a Reply in the ProvideResponse state
    let reply = scenario.to_provide_response();

    // Create an error response
    let response = Response::builder()
        .status(StatusCode::NOT_FOUND)
        .header("content-type", "text/plain")
        .body(())
        .unwrap();

    // Provide the response
    let reply = reply.provide(response).unwrap();

    // Verify the state transition
    assert!(reply.inner().response.is_some());
    assert!(reply.inner().state.writer.is_some());

    // Verify the status code
    let status = reply.inner().response.as_ref().unwrap().prelude().1;
    assert_eq!(status, StatusCode::NOT_FOUND);
}

#[test]
fn provide_response_after_reject_100() {
    // Create a scenario with a POST request with Expect: 100-continue
    let scenario = Scenario::builder()
        .post("/path")
        .header("expect", "100-continue")
        .build();

    // Get a Reply in the Send100 state
    let reply = scenario.to_send_100();

    // Reject the 100-continue request
    let reply = reply.reject();

    // Create an error response (required after rejecting 100-continue)
    let response = Response::builder()
        .status(StatusCode::BAD_REQUEST)
        .header("content-type", "text/plain")
        .body(())
        .unwrap();

    // Provide the response
    let reply = reply.provide(response).unwrap();

    // Verify the state transition
    assert!(reply.inner().response.is_some());
    assert!(reply.inner().state.writer.is_some());

    // Verify the status code
    let status = reply.inner().response.as_ref().unwrap().prelude().1;
    assert_eq!(status, StatusCode::BAD_REQUEST);
}

#[test]
fn error_when_providing_success_after_reject_100() {
    // Create a scenario with a POST request with Expect: 100-continue
    let scenario = Scenario::builder()
        .post("/path")
        .header("expect", "100-continue")
        .build();

    // Get a Reply in the Send100 state
    let reply = scenario.to_send_100();

    // Reject the 100-continue request
    let reply = reply.reject();

    // Create a successful response (invalid after rejecting 100-continue)
    let response = Response::builder()
        .status(StatusCode::OK)
        .header("content-type", "text/plain")
        .body(())
        .unwrap();

    // Provide the response (should fail)
    let result = reply.provide(response);

    // Verify the error
    assert!(result.is_err());
    match result.unwrap_err() {
        Error::BadReject100Status(status) => {
            assert_eq!(status, StatusCode::OK);
        }
        _ => panic!("Expected BadReject100Status error"),
    }
}

#[test]
fn provide_response_with_content_length() {
    // Create a scenario with a GET request
    let scenario = Scenario::builder().get("/path").build();

    // Get a Reply in the ProvideResponse state
    let reply = scenario.to_provide_response();

    // Create a response with content-length
    let response = Response::builder()
        .status(StatusCode::OK)
        .header("content-type", "text/plain")
        .header("content-length", "11")
        .body(())
        .unwrap();

    // Provide the response
    let reply = reply.provide(response).unwrap();

    // Verify the state transition
    assert!(reply.inner().response.is_some());
    assert!(reply.inner().state.writer.is_some());

    // Verify the content-length header is present
    let has_content_length = reply
        .inner()
        .response
        .as_ref()
        .unwrap()
        .headers()
        .any(|(name, value)| name == "content-length" && value == "11");
    assert!(has_content_length);
}

#[test]
fn provide_response_with_chunked_encoding() {
    // Create a scenario with a GET request
    let scenario = Scenario::builder().get("/path").build();

    // Get a Reply in the ProvideResponse state
    let reply = scenario.to_provide_response();

    // Create a response with chunked encoding
    let response = Response::builder()
        .status(StatusCode::OK)
        .header("content-type", "text/plain")
        .header("transfer-encoding", "chunked")
        .body(())
        .unwrap();

    // Provide the response
    let reply = reply.provide(response).unwrap();

    // Verify the state transition
    assert!(reply.inner().response.is_some());
    assert!(reply.inner().state.writer.is_some());

    // Verify the transfer-encoding header is present
    let has_chunked = reply
        .inner()
        .response
        .as_ref()
        .unwrap()
        .headers()
        .any(|(name, value)| name == "transfer-encoding" && value == "chunked");
    assert!(has_chunked);
}

#[test]
fn provide_response_with_signed_content_length_is_rejected() {
    let scenario = Scenario::builder().get("/path").build();
    let reply = scenario.to_provide_response();

    // Content-Length must be plain digits. Rust's integer parsing would
    // accept a leading sign, the HTTP grammar does not.
    let response = Response::builder()
        .status(StatusCode::OK)
        .header("content-length", "+42")
        .body(())
        .unwrap();

    let err = reply.provide(response).unwrap_err();
    assert_eq!(err, Error::BadContentLengthHeader);
}

#[test]
fn provide_response_with_content_length_list_is_rejected() {
    let scenario = Scenario::builder().get("/path").build();
    let reply = scenario.to_provide_response();

    // We control what we send. A comma separated list is not a valid
    // Content-Length to send (RFC 9110 ยง8.6: the value is 1*DIGIT), so it
    // must be rejected here rather than written to the wire verbatim.
    let response = Response::builder()
        .status(StatusCode::OK)
        .header("content-length", "42, 42")
        .body(())
        .unwrap();

    let err = reply.provide(response).unwrap_err();
    assert!(matches!(
        err,
        Error::BadContentLengthHeader | Error::TooManyContentLengthHeaders
    ));
}

#[test]
fn representation_headers_without_body() {
    for (method, status) in [("HEAD", 200), ("GET", 304)] {
        for (name, value) in [
            ("content-length", "1048576"),
            ("transfer-encoding", "chunked"),
            ("content-length", "0"),
        ] {
            let scenario = Scenario::builder()
                .method(method.parse().unwrap(), "/")
                .build();
            let response = Response::builder()
                .status(status)
                .header(name, value)
                .body(())
                .unwrap();
            let mut reply = scenario.to_provide_response().provide(response).unwrap();
            let mut output = [0; 1024];
            let n = reply.write(&mut output).unwrap();
            let head = std::str::from_utf8(&output[..n]).unwrap();
            assert!(head.contains(&format!("{name}: {value}\r\n")));
            assert!(head.ends_with("\r\n\r\n"));
            assert!(reply.is_finished());
            assert!(matches!(
                reply.proceed(),
                crate::server::SendResponseResult::Cleanup(_)
            ));
        }
    }
}

#[test]
fn representation_headers_reject_content_length_and_transfer_encoding_together() {
    for (method, status) in [("HEAD", 200), ("GET", 304)] {
        let scenario = Scenario::builder()
            .method(method.parse().unwrap(), "/")
            .build();
        let response = Response::builder()
            .status(status)
            .header("content-length", "1048576")
            .header("transfer-encoding", "chunked")
            .body(())
            .unwrap();

        assert!(
            scenario.to_provide_response().provide(response).is_err(),
            "{method} {status} must reject conflicting framing headers"
        );
    }
}

#[test]
fn forbidden_status_body_headers_still_rejected() {
    for method in ["GET", "HEAD"] {
        for status in [100, 101, 204] {
            let scenario = Scenario::builder()
                .method(method.parse().unwrap(), "/")
                .build();
            let response = Response::builder()
                .status(status)
                .header("content-length", "1048576")
                .body(())
                .unwrap();
            assert_eq!(
                scenario
                    .to_provide_response()
                    .provide(response)
                    .unwrap_err(),
                Error::BodyNotAllowed
            );
        }
    }
}