mod common;
use indexmap::IndexMap;
use predicates::prelude::predicate;
use reqwest::StatusCode;
use rstest::rstest;
use serde_json::json;
use slumber_core::{database::Database, http::ExchangeSummary};
use slumber_util::assert_matches;
use wiremock::{Mock, MockServer, Request, ResponseTemplate, matchers};
#[tokio::test]
async fn test_request() {
let server = MockServer::start().await;
let host = server.uri();
let body = json!({
"username": "username2",
"name": "Frederick Smidgen"
});
Mock::given(matchers::method("POST"))
.and(matchers::path("/json"))
.and(matchers::body_json(&body))
.respond_with(ResponseTemplate::new(200).set_body_json(&body))
.mount(&server)
.await;
let (mut command, data_dir) = common::slumber();
command.args(["request", "jsonBody", "--profile", "profile2"]);
command
.env("HOST", host)
.assert()
.success()
.stdout(body.to_string());
let database = Database::from_directory(&data_dir).unwrap();
assert_eq!(
&database.get_all_requests().unwrap(),
&[],
"Expected request to not be persisted"
);
}
#[rstest]
#[case::overwrite(
&["--override", "a=1", "--override", "b=2"], r#"{"a":"1","b":"2"}"#
)]
#[case::alias(&["-o", "a=1"], r#"{"a":"1","b":0}"#)]
#[case::duplicate(&["-o", "a=1", "-o", "a=2"], r#"{"a":"2","b":0}"#)]
#[tokio::test]
async fn test_request_override_profile(
#[case] args: &[&str],
#[case] expected_body: &'static str,
) {
let server = MockServer::start().await;
let host = server.uri();
Mock::given(matchers::method("POST"))
.and(matchers::path("/override"))
.respond_with(echo_body)
.mount(&server)
.await;
let (mut command, _) = common::slumber();
command
.args(["request", "override", "--exit-status"])
.args(args)
.env("HOST", host)
.assert()
.success()
.stdout(expected_body);
}
#[rstest]
#[case::overwrite(
&["--url", "{{ host }}/override2?q=1"],
// Inherits query params from the recipe
"http://localhost/override2?q=1&foo=bar&many=baz&many=blorp",
)]
#[tokio::test]
async fn test_request_override_url(
#[case] args: &[&str],
#[case] expected_url: &'static str,
) {
let server = MockServer::start().await;
let host = server.uri();
Mock::given(matchers::method("POST"))
.and(matchers::path("/override2"))
.respond_with(|req: &Request| {
ResponseTemplate::new(200).set_body_string(req.url.to_string())
})
.mount(&server)
.await;
let (mut command, _) = common::slumber();
command
.args(["request", "override", "--exit-status"])
.args(args)
.env("HOST", host)
.assert()
.success()
.stdout(expected_url);
}
#[rstest]
#[case::overwrite_one_to_one(
&["--query", "foo=over"], "foo=over&many=baz&many=blorp",
)]
#[case::overwrite_one_to_two(
&["--query", "foo=one", "--query", "foo=two"],
// The ordering is because first instance of foo is a replacement while the
// second is an insert. This is determined by the HTTP engine implementation
"foo=one&many=baz&many=blorp&foo=two",
)]
#[case::overwrite_two_to_one(&["--query", "many=over"], "foo=bar&many=over")]
#[case::additional(
&["--query", "add=over"], "foo=bar&many=baz&many=blorp&add=over",
)]
#[case::omit(&["--query", "many"], "foo=bar")]
#[tokio::test]
async fn test_request_override_query(
#[case] args: &[&str],
#[case] expected_query: &'static str,
) {
let server = MockServer::start().await;
let host = server.uri();
Mock::given(matchers::method("POST"))
.and(matchers::path("/override"))
.respond_with(|req: &Request| {
let query = req.url.query().expect("Missing query string");
ResponseTemplate::new(200).set_body_string(query)
})
.mount(&server)
.await;
let (mut command, _) = common::slumber();
command
.args(["request", "override", "--exit-status"])
.args(args)
.env("HOST", host)
.assert()
.success()
.stdout(expected_query);
}
#[rstest]
#[case::overwrite(&["--header", "x-test=over"], &[("x-test", Some("over"))])]
#[case::alias(&["-H", "x-test=over"], &[("x-test", Some("over"))])]
#[case::duplicate(
// Second replaces first
&["-H", "x-test=1", "-H", "x-test=2"], &[("x-test", Some("2"))],
)]
#[case::additional(&["--header", "x-new=over"], &[("x-new", Some("over"))])]
#[case::omit(&["--header", "x-test"], &[("x-test", None)])]
#[tokio::test]
async fn test_request_override_header(
#[case] args: &[&str],
#[case] expected_headers: &[(&str, Option<&str>)],
) {
type Headers<'a> = IndexMap<&'a str, &'a str>;
let server = MockServer::start().await;
let host = server.uri();
Mock::given(matchers::method("POST"))
.and(matchers::path("/override"))
.respond_with(|req: &Request| {
let headers: Headers = req
.headers
.iter()
.map(|(k, v)| (k.as_str(), v.to_str().unwrap()))
.collect();
ResponseTemplate::new(200).set_body_json(&headers)
})
.mount(&server)
.await;
let (mut command, _) = common::slumber();
let assert = command
.args(["request", "override", "--exit-status"])
.args(args)
.env("HOST", host)
.assert()
.success();
let actual: Headers =
serde_json::from_slice(&assert.get_output().stdout).unwrap();
for (header, expected) in expected_headers {
assert_eq!(actual.get(header).copied(), *expected, "header `{header}`");
}
}
#[rstest]
#[case::overwrite(&["--body", r#"{"a":"{{ a }}"}"#], r#"{"a":0}"#)]
#[case::alias(&["--data", r#"{"a":"{{ a }}"}"#], r#"{"a":0}"#)] #[tokio::test]
async fn test_request_override_body(
#[case] args: &[&str],
#[case] expected_body: &'static str,
) {
let server = MockServer::start().await;
let host = server.uri();
Mock::given(matchers::method("POST"))
.and(matchers::path("/override"))
.respond_with(echo_body)
.mount(&server)
.await;
let (mut command, _) = common::slumber();
command
.args(["request", "override", "--exit-status"])
.args(args)
.env("HOST", host)
.assert()
.success()
.stdout(expected_body);
}
#[rstest]
#[case::overwrite(&["--form", "username=over"], &[("username", "over")])]
#[case::alias(&["-F", "username=over"], &[("username", "over")])]
#[case::additional(
&["--form", "new=over"], &[("username", "username1"), ("new", "over")],
)]
#[case::duplicate(
// Second replaces first
&["-F", "new=1", "-F", "new=2"], &[("username", "username1"), ("new", "2")],
)]
#[case::omit(&["--form", "username"], &[])]
#[tokio::test]
async fn test_request_override_form(
#[case] args: &[&str],
#[case] expected_form: &[(&str, &str)],
) {
let server = MockServer::start().await;
let host = server.uri();
Mock::given(matchers::method("POST"))
.and(matchers::path("/urlencoded"))
.respond_with(echo_body)
.mount(&server)
.await;
let (mut command, _) = common::slumber();
let assert = command
.args(["request", "urlencoded", "--exit-status"])
.args(args)
.env("HOST", host)
.assert()
.success();
let actual: Vec<(&str, &str)> =
serde_urlencoded::from_bytes(&assert.get_output().stdout).unwrap();
assert_eq!(&actual, expected_form);
}
#[rstest]
#[case::basic(&["--basic", "user:hunter2"], "Basic dXNlcjpodW50ZXIy")]
#[case::basic_alias(&["--user", "user:hunter2"], "Basic dXNlcjpodW50ZXIy")]
#[case::token(&["--bearer", "my-token"], "Bearer my-token")]
#[case::token_alias(&["--token", "my-token"], "Bearer my-token")]
#[tokio::test]
async fn test_request_override_auth(
#[case] args: &[&str],
#[case] expected_auth: &'static str,
) {
let server = MockServer::start().await;
let host = server.uri();
Mock::given(matchers::method("POST"))
.and(matchers::path("/override"))
.respond_with(|req: &Request| {
let auth = req
.headers
.get("Authorization")
.map(|value| value.to_str().unwrap())
.unwrap_or_default();
ResponseTemplate::new(200).set_body_string(auth)
})
.mount(&server)
.await;
let (mut command, _) = common::slumber();
command
.args(["request", "override", "--exit-status"])
.args(args)
.env("HOST", host)
.assert()
.success()
.stdout(expected_auth);
}
#[rstest]
#[case::mutually_exclusive(
&["--basic", "user:pass", "--bearer", "token"],
"the argument '--basic <username:password>' cannot be used with \
'--bearer <token>'",
)]
#[case::invalid_template(&["--bearer", "{{unclosed"], "invalid expression")]
#[case::basic_prompt(&["--basic", "user"], "No reply")]
#[tokio::test]
async fn test_request_override_auth_error(
#[case] args: &[&str],
#[case] expected_error: &'static str,
) {
let (mut command, _) = common::slumber();
command
.args(["request", "override", "--exit-status"])
.args(args)
.assert()
.failure()
.stderr(predicate::str::contains(expected_error));
}
#[tokio::test]
async fn test_request_verbose() {
let server = MockServer::start().await;
let host = server.uri();
let body = json!({
"username": "username1",
"name": "Frederick Smidgen"
});
Mock::given(matchers::method("POST"))
.and(matchers::path("/json"))
.and(matchers::body_json(&body))
.respond_with(ResponseTemplate::new(200).set_body_json(&body))
.mount(&server)
.await;
let (mut command, _) = common::slumber();
command.args([
"request",
"jsonBody",
"--verbose",
"-o",
&format!("host={host}"),
]);
command.assert().success().stdout(body.to_string());
}
#[tokio::test]
async fn test_request_dry_run() {
let (mut command, _) = common::slumber();
command.args(["request", "jsonBody", "--dry-run"]);
command.assert().success().stderr(
"> POST http://server/json HTTP/1.1
> content-type: application/json
> {\"username\":\"username1\",\"name\":\"Frederick Smidgen\"}
",
);
}
#[tokio::test]
async fn test_request_exit_status() {
let server = MockServer::start().await;
let host = server.uri();
let body = json!({
"username": "username1",
"name": "Frederick Smidgen"
});
Mock::given(matchers::method("POST"))
.and(matchers::path("/json"))
.and(matchers::body_json(&body))
.respond_with(ResponseTemplate::new(400).set_body_json(&body))
.mount(&server)
.await;
let (mut command, _) = common::slumber();
command.args([
"request",
"jsonBody",
"--exit-status",
"-o",
&format!("host={host}"),
]);
command.assert().failure().stdout(body.to_string());
}
#[tokio::test]
async fn test_request_persist() {
let server = MockServer::start().await;
let host = server.uri();
let body = json!({
"username": "username1",
"name": "Frederick Smidgen"
});
Mock::given(matchers::method("GET"))
.and(matchers::path("/users/username1"))
.respond_with(ResponseTemplate::new(200).set_body_json(&body))
.mount(&server)
.await;
Mock::given(matchers::method("GET"))
.and(matchers::path("/chained/username1"))
.respond_with(ResponseTemplate::new(200).set_body_json(&body))
.mount(&server)
.await;
let (mut command, data_dir) = common::slumber();
command.args([
"request",
"chained",
"--persist",
"-o",
&format!("host={host}"),
]);
command.assert().success().stdout(body.to_string());
let database = Database::from_directory(&data_dir).unwrap();
let requests = database.get_all_requests().unwrap();
let (recipe_id, profile_id, status) = assert_matches!(
requests.as_slice(),
[ExchangeSummary {
recipe_id,
profile_id,
status,
..
}] => (recipe_id, profile_id, status),
);
assert_eq!(recipe_id, &"chained".into());
assert_eq!(profile_id, &Some("profile1".into()));
assert_eq!(*status, StatusCode::OK);
}
#[tokio::test]
async fn test_request_persist_disabled() {
let server = MockServer::start().await;
let host = server.uri();
Mock::given(matchers::method("GET"))
.and(matchers::path("/secret"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
let (mut command, data_dir) = common::slumber();
command.args([
"request",
"do_not_persist",
"--persist",
"-o",
&format!("host={host}"),
]);
command.assert().success();
let database = Database::from_directory(&data_dir).unwrap();
let requests = database.get_all_requests().unwrap();
let (recipe_id, profile_id, status) = assert_matches!(
requests.as_slice(),
[ExchangeSummary {
recipe_id,
profile_id,
status,
..
}] => (recipe_id, profile_id, status),
);
assert_eq!(recipe_id, &"do_not_persist".into());
assert_eq!(profile_id, &Some("profile1".into()));
assert_eq!(*status, StatusCode::OK);
}
#[tokio::test]
async fn test_set_collection_name() {
let (mut command, data_dir) = common::slumber();
let database = Database::from_directory(&data_dir).unwrap();
assert_eq!(database.get_collections().unwrap().as_slice(), &[]);
command.args(["request", "jsonBody", "--dry-run"]);
command.assert().success();
assert_eq!(
database.get_collections().unwrap()[0].name.as_deref(),
Some("CLI Tests")
);
}
fn echo_body(req: &Request) -> ResponseTemplate {
ResponseTemplate::new(200).set_body_bytes(req.body.clone())
}