use super::ctx::Ctx;
use crate::runtime::report::Event;
use anyhow::{Context, bail};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
#[derive(Clone)]
pub struct HttpResponse {
pub status: i64,
pub headers: HashMap<String, String>, pub body: String,
ctx: Arc<Ctx>,
}
impl HttpResponse {
pub fn status(&self) -> i64 {
super::ctx::mark_pending_label("HTTP status");
self.status
}
pub fn body(&self) -> String {
super::ctx::mark_pending_label("HTTP body");
self.body.clone()
}
pub fn header(&self, name: &str) -> Option<String> {
super::ctx::mark_pending_label(format!("HTTP header {name}"));
self.headers.get(&name.to_lowercase()).cloned()
}
pub fn json(&self, path: &str) -> Result<serde_json::Value, String> {
super::ctx::mark_pending_label(if path.is_empty() {
"HTTP body (JSON)".to_string()
} else {
format!("HTTP {path}")
});
json_path_value(&self.body, path).map_err(|e| e.to_string())
}
pub fn expect_status(&self, code: i64) -> Result<(), String> {
let ok = self.status == code;
self.ctx.emit(&Event::Assertion {
label: None,
expect: format!("status is {code}"),
ok,
actual: Some(format!("status {}", self.status)),
});
if ok {
Ok(())
} else {
Err(format!("expected status {code}, got {}", self.status))
}
}
}
fn error_chain(e: &dyn std::error::Error) -> String {
let mut msg = e.to_string();
let mut src = e.source();
while let Some(s) = src {
msg.push_str(&format!(": {s}"));
src = s.source();
}
msg
}
pub fn perform(
ctx: &Arc<Ctx>,
method: &str,
url: &str,
headers: &[(String, String)],
body: Option<String>,
) -> Result<HttpResponse, String> {
let mut builder = reqwest::blocking::Client::builder().timeout(Duration::from_secs(30));
if ctx.http_insecure() {
builder = builder.danger_accept_invalid_certs(true);
}
let client = builder
.build()
.map_err(|e| format!("build HTTP client: {e}"))?;
let m = reqwest::Method::from_bytes(method.as_bytes())
.map_err(|_| format!("invalid HTTP method `{method}`"))?;
let mut req = client.request(m, url);
for (k, v) in headers {
req = req.header(k.as_str(), v.as_str());
}
if let Some(b) = body {
req = req.body(b);
}
let resp = req
.send()
.map_err(|e| format!("HTTP request to {url}: {}", error_chain(&e)))?;
let status = resp.status().as_u16() as i64;
let resp_headers = resp
.headers()
.iter()
.map(|(k, v)| {
(
k.as_str().to_lowercase(),
v.to_str().unwrap_or("").to_string(),
)
})
.collect();
let body = resp
.text()
.map_err(|e| format!("read HTTP response body: {e}"))?;
ctx.emit(&Event::Http {
method,
url,
status: status as u16,
});
Ok(HttpResponse {
status,
headers: resp_headers,
body,
ctx: ctx.clone(),
})
}
fn json_path_value(body: &str, path: &str) -> anyhow::Result<serde_json::Value> {
let root: serde_json::Value = if body.trim().is_empty() {
serde_json::Value::Null
} else {
serde_json::from_str(body).context("response body is not valid JSON")?
};
let mut cur = &root;
for seg in path.split('.').filter(|s| !s.is_empty()) {
cur = match cur {
serde_json::Value::Object(map) => map
.get(seg)
.with_context(|| format!("no field `{seg}` in JSON path `{path}`"))?,
serde_json::Value::Array(arr) => {
let i: usize = seg
.parse()
.with_context(|| format!("`{seg}` is not an array index in `{path}`"))?;
arr.get(i)
.with_context(|| format!("index {i} out of range in JSON path `{path}`"))?
}
_ => bail!("JSON path `{path}` descends past a scalar at `{seg}`"),
};
}
Ok(cur.clone())
}
#[cfg(test)]
mod tests {
use super::json_path_value;
const BODY: &str = r#"{"state":"ringing","nested":{"id":42},"items":["a","b"]}"#;
#[test]
fn json_path_navigates_objects_and_arrays() {
use serde_json::json;
assert_eq!(json_path_value(BODY, "state").unwrap(), json!("ringing"));
assert_eq!(json_path_value(BODY, "nested.id").unwrap(), json!(42));
assert_eq!(json_path_value(BODY, "items.1").unwrap(), json!("b"));
assert_eq!(json_path_value(BODY, "nested").unwrap(), json!({"id": 42}));
assert!(json_path_value(BODY, "").unwrap().is_object());
}
#[test]
fn json_path_errors_are_descriptive() {
assert!(json_path_value(BODY, "missing").is_err());
assert!(json_path_value(BODY, "items.9").is_err());
assert!(json_path_value(BODY, "state.x").is_err()); assert!(json_path_value("not json", "x").is_err()); }
#[test]
fn empty_body_is_json_null() {
use serde_json::json;
assert_eq!(json_path_value("", "").unwrap(), json!(null));
assert_eq!(json_path_value(" \n", "").unwrap(), json!(null));
}
}