ehttpd 0.14.0

A HTTP server nano-framework, which can be used to create custom small-scale HTTP server applications
Documentation
use ehttpd::bytes::Data;
use ehttpd::http::Response;
use std::fs::{self, File};
use std::io::{Seek, SeekFrom, Write};
use std::path::Path;
use std::process;

#[test]
fn response_is_written() {
    // A response with a body and content type
    let mut response = Response::new_status_reason(201, "Created");
    response.set_body_data("hello");
    response.set_content_type("text/plain");

    // Write the response
    let mut output = Vec::new();
    response.to_stream(&mut output).expect("failed to write response");

    // Ensure that the complete response is written
    let expected = concat! {
        "HTTP/1.1 201 Created\r\n",
        "Content-Length: 5\r\n",
        "Content-Type: text/plain\r\n",
        "\r\n",
        "hello",
    };
    assert_eq!(output, expected.as_bytes());
}

#[test]
fn field_is_replaced_case_insensitively() {
    // A response whose field is replaced with different casing
    let mut response = Response::new_200_ok();
    response.set_field("X-Test", "first");
    response.set_field("x-test", "second");

    // Ensure that only the replacement remains
    let mut fields = response.fields.iter().filter(|(key, _)| key.eq_ignore_ascii_case(b"X-Test"));
    let field = fields.next().expect("response field is missing");
    assert!(fields.next().is_none());
    assert_eq!(field.0, b"x-test");
    assert_eq!(field.1, b"second");
}

#[test]
fn connection_close_is_recognized_inside_a_token_list() {
    // Ensure the close-header is detected
    let mut response = Response::new_200_ok();
    response.set_field("Connection", "keep-alive, Close, upgrade");
    assert!(response.has_connection_close());
}

#[test]
fn head_response_preserves_content_length() {
    // A response with a body
    let mut response = Response::new_200_ok();
    response.set_body_data("hello");

    // Convert the response to a HEAD response and write it
    response.make_head();
    let mut output = Vec::new();
    response.to_stream(&mut output).expect("failed to write response");

    // Ensure that the body is omitted and its length is preserved
    let expected = concat! {
        "HTTP/1.1 200 OK\r\n",
        "Content-Length: 5\r\n",
        "\r\n",
    };
    assert_eq!(output, expected.as_bytes());
}

#[test]
fn status_helpers_set_status_and_reason() {
    // Responses created by the status helpers
    let responses = [
        (Response::new_200_ok(), "200", "OK"),
        (Response::new_400_badrequest(), "400", "Bad Request"),
        (Response::new_403_forbidden(), "403", "Forbidden"),
        (Response::new_404_notfound(), "404", "Not Found"),
        (Response::new_405_methodnotallowed(), "405", "Method Not Allowed"),
        (Response::new_413_payloadtoolarge(), "413", "Payload Too Large"),
        (Response::new_416_rangenotsatisfiable(), "416", "Range Not Satisfiable"),
        (Response::new_500_internalservererror(), "500", "Internal Server Error"),
    ];

    // Ensure that each helper sets the expected status line and empty body
    for (response, status, reason) in responses {
        assert_eq!(response.version, b"HTTP/1.1");
        assert_eq!(response.status, status);
        assert_eq!(response.reason, reason);
        assert_eq!(response.content_length().expect("invalid content length"), Some(0));
    }
}

#[test]
fn redirect_and_authentication_helpers_set_fields() {
    // Responses created by helpers that set additional fields
    let responses = [
        (Response::new_303_seeother("/next"), "Location", "/next"),
        (Response::new_307_temporaryredirect("/later"), "Location", "/later"),
        (Response::new_401_unauthorized("Basic"), "WWW-Authenticate", "Basic"),
    ];

    // Ensure that each helper sets its additional field
    for (response, expected_key, expected_value) in responses {
        let (_, value) = (response.fields)
            .iter()
            .find(|(key, _)| key.eq_ignore_ascii_case(expected_key.as_bytes()))
            .expect("response field is missing");
        assert_eq!(value, expected_value);
    }
}

#[test]
fn file_body_uses_current_offset() {
    // A file positioned after its first byte
    let path = Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!("ehttpd-response-{}.tmp", process::id()));
    let mut file = File::options()
        .read(true)
        .write(true)
        .create(true)
        .truncate(true)
        .open(&path)
        .expect("failed to create response file");
    file.write_all(b"<BODY>").expect("failed to write response file");
    file.seek(SeekFrom::Start(2)).expect("failed to seek response file");

    // Set the file as response body and write it
    let mut response = Response::new_200_ok();
    response.set_body_file(file).expect("failed to set file body");
    let mut output = Vec::new();
    response.to_stream(&mut output).expect("failed to write response");

    // Close and remove the response file
    drop(response);
    fs::remove_file(path).expect("failed to remove response file");

    // Ensure that only the remaining file content is written
    let expected = concat! {
        "HTTP/1.1 200 OK\r\n",
        "Content-Length: 4\r\n",
        "\r\n",
        "ODY>",
    };
    assert_eq!(output, expected.as_bytes());
}

#[test]
fn invalid_content_length_is_rejected() {
    // Ensure that the content length is rejected
    let mut response = Response::new(Data::from(b"HTTP/1.1"), Data::from(b"200"), Data::from(b"OK"));
    response.set_field("Content-Length", "invalid");
    assert!(response.content_length().is_err());
}