barehttp 0.0.1

A minimal, explicit HTTP client for Rust with no_std support and blocking I/O
Documentation
use crate::client::policy::{PolicyDecision, RequestPolicy};
use crate::config::{Config, HttpStatusHandling, ProtocolRestriction, RedirectPolicy};
use crate::error::Error;
use crate::headers::Headers;
use crate::method::Method;
use crate::parser::uri::Uri;
use crate::transport::RawResponse;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
extern crate alloc;

fn make_redirect_response(
  status: u16,
  location: &str,
) -> RawResponse {
  let mut headers = Headers::new();
  headers.insert("Location", location);
  RawResponse {
    status_code: status,
    reason: String::from("Redirect"),
    headers,
    body_bytes: Vec::new(),
  }
}

#[test]
fn https_only_policy_rejects_http() {
  let policy = RequestPolicy::new(&Config {
    protocol_restriction: ProtocolRestriction::HttpsOnly,
    ..Default::default()
  });

  let uri = Uri::parse("http://example.com").unwrap();
  let result = policy.validate_protocol(&uri);

  assert!(matches!(result, Err(Error::HttpsRequired)));
}

#[test]
fn https_only_policy_allows_https() {
  let policy = RequestPolicy::new(&Config {
    protocol_restriction: ProtocolRestriction::HttpsOnly,
    ..Default::default()
  });

  let uri = Uri::parse("https://example.com").unwrap();
  assert!(policy.validate_protocol(&uri).is_ok());
}

#[test]
fn policy_drops_body_for_head_requests() {
  let mut policy = RequestPolicy::new(&Config::default());

  let mut headers = Headers::new();
  headers.insert("Content-Length", "10");

  let raw = RawResponse {
    status_code: 200,
    reason: String::from("OK"),
    headers,
    body_bytes: b"1234567890".to_vec(),
  };

  let decision = policy
    .process_raw_response(
      raw,
      &Uri::parse("http://example.com").unwrap(),
      "http://example.com",
      Method::Head,
      None,
    )
    .unwrap();

  match decision {
    PolicyDecision::Return(resp) => {
      assert_eq!(resp.status_code, 200);
      assert!(resp.body.as_bytes().is_empty(), "HEAD response body should be empty");
    },
    PolicyDecision::Redirect { .. } => panic!("Expected PolicyDecision::Return"),
  }
}

#[test]
fn post_302_redirect_becomes_get() {
  let mut policy = RequestPolicy::new(&Config::default());

  let raw = make_redirect_response(302, "/next");

  let decision = policy
    .process_raw_response(
      raw,
      &Uri::parse("http://a.com").unwrap(),
      "http://a.com",
      Method::Post,
      Some(vec![1, 2, 3]),
    )
    .unwrap();

  match decision {
    PolicyDecision::Redirect {
      next_method, next_body, ..
    } => {
      assert_eq!(next_method, Method::Get, "POST 302 should become GET");
      assert!(next_body.is_none(), "GET should not have body");
    },
    PolicyDecision::Return(_) => panic!("Expected PolicyDecision::Redirect"),
  }
}

#[test]
fn post_301_redirect_becomes_get() {
  let mut policy = RequestPolicy::new(&Config::default());

  let raw = make_redirect_response(301, "/next");

  let decision = policy
    .process_raw_response(
      raw,
      &Uri::parse("http://a.com").unwrap(),
      "http://a.com",
      Method::Post,
      Some(vec![1, 2, 3]),
    )
    .unwrap();

  match decision {
    PolicyDecision::Redirect {
      next_method, next_body, ..
    } => {
      assert_eq!(next_method, Method::Get);
      assert!(next_body.is_none());
    },
    PolicyDecision::Return(_) => panic!("Expected PolicyDecision::Redirect"),
  }
}

#[test]
fn post_303_redirect_becomes_get() {
  let mut policy = RequestPolicy::new(&Config::default());

  let raw = make_redirect_response(303, "/next");

  let decision = policy
    .process_raw_response(
      raw,
      &Uri::parse("http://a.com").unwrap(),
      "http://a.com",
      Method::Post,
      Some(vec![1, 2, 3]),
    )
    .unwrap();

  match decision {
    PolicyDecision::Redirect {
      next_method, next_body, ..
    } => {
      assert_eq!(next_method, Method::Get);
      assert!(next_body.is_none());
    },
    PolicyDecision::Return(_) => panic!("Expected PolicyDecision::Redirect"),
  }
}

#[test]
fn get_redirect_stays_get() {
  let mut policy = RequestPolicy::new(&Config::default());

  let raw = make_redirect_response(302, "/next");

  let decision = policy
    .process_raw_response(
      raw,
      &Uri::parse("http://a.com").unwrap(),
      "http://a.com",
      Method::Get,
      None,
    )
    .unwrap();

  match decision {
    PolicyDecision::Redirect { next_method, .. } => {
      assert_eq!(next_method, Method::Get);
    },
    PolicyDecision::Return(_) => panic!("Expected PolicyDecision::Redirect"),
  }
}

#[test]
fn redirect_loop_is_detected() {
  let mut policy = RequestPolicy::new(&Config::default());

  let raw = make_redirect_response(301, "http://a.com");
  let uri = Uri::parse("http://a.com").unwrap();

  policy
    .process_raw_response(raw.clone(), &uri, "http://a.com", Method::Get, None)
    .unwrap();

  let err = policy
    .process_raw_response(raw, &uri, "http://a.com", Method::Get, None)
    .unwrap_err();

  assert!(matches!(err, Error::RedirectLoop));
}

#[test]
fn status_4xx_is_error_when_configured() {
  let mut policy = RequestPolicy::new(&Config {
    http_status_handling: HttpStatusHandling::AsError,
    ..Default::default()
  });

  let raw = RawResponse {
    status_code: 404,
    reason: String::from("Not Found"),
    headers: Headers::new(),
    body_bytes: Vec::new(),
  };

  let err = policy
    .process_raw_response(
      raw,
      &Uri::parse("http://example.com").unwrap(),
      "http://example.com",
      Method::Get,
      None,
    )
    .unwrap_err();

  assert!(matches!(err, Error::HttpStatus(404)));
}

#[test]
fn status_5xx_is_error_when_configured() {
  let mut policy = RequestPolicy::new(&Config {
    http_status_handling: HttpStatusHandling::AsError,
    ..Default::default()
  });

  let raw = RawResponse {
    status_code: 500,
    reason: String::from("Internal Server Error"),
    headers: Headers::new(),
    body_bytes: Vec::new(),
  };

  let err = policy
    .process_raw_response(
      raw,
      &Uri::parse("http://example.com").unwrap(),
      "http://example.com",
      Method::Get,
      None,
    )
    .unwrap_err();

  assert!(matches!(err, Error::HttpStatus(500)));
}

#[test]
fn status_4xx_is_ok_when_configured_as_response() {
  let mut policy = RequestPolicy::new(&Config {
    http_status_handling: HttpStatusHandling::AsResponse,
    ..Default::default()
  });

  let raw = RawResponse {
    status_code: 404,
    reason: String::from("Not Found"),
    headers: Headers::new(),
    body_bytes: Vec::new(),
  };

  let result = policy.process_raw_response(
    raw,
    &Uri::parse("http://example.com").unwrap(),
    "http://example.com",
    Method::Get,
    None,
  );

  assert!(result.is_ok());
  match result.unwrap() {
    PolicyDecision::Return(resp) => assert_eq!(resp.status_code, 404),
    PolicyDecision::Redirect { .. } => panic!("Expected PolicyDecision::Return"),
  }
}

#[test]
fn too_many_redirects_is_error() {
  let mut policy = RequestPolicy::new(&Config {
    max_redirects: 2,
    ..Default::default()
  });

  let raw = make_redirect_response(301, "/next");

  policy
    .process_raw_response(
      raw.clone(),
      &Uri::parse("http://a.com").unwrap(),
      "http://a.com",
      Method::Get,
      None,
    )
    .unwrap();

  policy
    .process_raw_response(
      raw.clone(),
      &Uri::parse("http://b.com").unwrap(),
      "http://b.com",
      Method::Get,
      None,
    )
    .unwrap();

  let err = policy
    .process_raw_response(
      raw,
      &Uri::parse("http://c.com").unwrap(),
      "http://c.com",
      Method::Get,
      None,
    )
    .unwrap_err();

  assert!(matches!(err, Error::TooManyRedirects));
}

#[test]
fn no_follow_policy_returns_redirect_response() {
  let mut policy = RequestPolicy::new(&Config {
    redirect_policy: RedirectPolicy::NoFollow,
    ..Default::default()
  });

  let raw = make_redirect_response(302, "/next");

  let result = policy.process_raw_response(
    raw,
    &Uri::parse("http://a.com").unwrap(),
    "http://a.com",
    Method::Get,
    None,
  );

  match result.unwrap() {
    PolicyDecision::Return(resp) => assert_eq!(resp.status_code, 302),
    PolicyDecision::Redirect { .. } => {
      panic!("Should not follow redirect with NoFollow policy")
    },
  }
}