surrealkit 0.5.8

Manage migrations, seeding and tests for your SurrealDB via CLI
use std::time::Duration;

use anyhow::{Context, Result, anyhow, bail};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde_json::Value;

use super::actors::ActorSession;
use super::assertions::{
	JsonAssertionContext, assert_header_value, assert_json_value_with_context,
};
use super::types::{ApiRequestCase, AssertionReport};

#[derive(Debug, Clone)]
pub struct ApiResult {
	pub status: u16,
	pub assertions: Vec<AssertionReport>,
}

pub async fn execute_api_case(
	base_url: &str,
	case: &ApiRequestCase,
	actor: &ActorSession,
	default_timeout_ms: u64,
) -> Result<ApiResult> {
	let client = reqwest::Client::builder()
		.timeout(Duration::from_millis(case.timeout_ms.unwrap_or(default_timeout_ms)))
		.build()
		.context("building API client")?;

	let path = case.path.trim();
	if path.is_empty() {
		bail!("api_request case path cannot be empty");
	}
	let url = format!(
		"{}{}{}",
		base_url.trim_end_matches('/'),
		if path.starts_with('/') {
			""
		} else {
			"/"
		},
		path
	);

	let method = reqwest::Method::from_bytes(case.method.to_uppercase().as_bytes())
		.with_context(|| format!("invalid HTTP method '{}'", case.method))?;

	let mut headers = HeaderMap::new();
	for (k, v) in &actor.headers {
		insert_header(&mut headers, k, v)?;
	}
	for (k, v) in &case.headers {
		insert_header(&mut headers, k, v)?;
	}

	let mut req = client.request(method, &url).headers(headers);
	if let Some(body) = &case.body {
		req = req.json(body);
	}

	let resp = req.send().await.with_context(|| format!("request to {} failed", url))?;
	let status = resp.status().as_u16();
	let headers = resp.headers().clone();
	let body_text = resp.text().await.context("reading response body")?;

	let mut assertions = Vec::new();
	let status_ok = status == case.expected_status;
	assertions.push(AssertionReport {
		name: "status".to_string(),
		passed: status_ok,
		message: format!("expected status {}, got {}", case.expected_status, status),
	});

	let body = if body_text.trim().is_empty() {
		None
	} else {
		serde_json::from_str::<Value>(&body_text).ok()
	};

	for (idx, assertion) in case.header_assertions.iter().enumerate() {
		assertions.push(assert_header_value(&headers, assertion, idx)?);
	}

	if !case.body_assertions.is_empty() {
		let parsed = body.as_ref().ok_or_else(|| {
			anyhow!("body assertions requested but response body is not valid JSON")
		})?;
		let ctx = JsonAssertionContext {
			actor_auth: actor.auth.clone(),
		};
		for (idx, assertion) in case.body_assertions.iter().enumerate() {
			assertions.push(assert_json_value_with_context(parsed, assertion, idx, &ctx)?);
		}
	}

	Ok(ApiResult {
		status,
		assertions,
	})
}

fn insert_header(headers: &mut HeaderMap, key: &str, value: &str) -> Result<()> {
	let name = HeaderName::from_bytes(key.as_bytes())
		.with_context(|| format!("invalid header name '{}'", key))?;
	let val = HeaderValue::from_str(value)
		.with_context(|| format!("invalid header value for '{}'", key))?;
	headers.insert(name, val);
	Ok(())
}