mod support;
use std::{
io::{Read, Write},
net::TcpListener,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
thread,
};
use support::TestWorkspace;
#[test]
fn exports_all_requests_non_interactively_when_selector_is_all() {
let workspace = TestWorkspace::new();
workspace.copy_fixture("non_interactive/multi-request.hen", "collection.hen");
let output = workspace.run_hen([
"run",
"collection.hen",
"all",
"--non-interactive",
"--export",
]);
assert_eq!(output.status_code, 0, "stderr: {}", output.stderr);
assert!(output
.stdout
.contains("curl -X GET 'https://example.com/one'"));
assert!(output
.stdout
.contains("curl -X GET 'https://example.com/two'"));
assert!(output.stderr.is_empty(), "stderr: {}", output.stderr);
}
#[test]
fn requires_selector_for_multi_request_collection_in_non_interactive_mode() {
let workspace = TestWorkspace::new();
workspace.copy_fixture("non_interactive/multi-request.hen", "collection.hen");
let output = workspace.run_hen(["run", "collection.hen", "--non-interactive"]);
assert_eq!(output.status_code, 2, "stderr: {}", output.stderr);
assert!(output.stdout.is_empty(), "stdout: {}", output.stdout);
assert!(output
.stderr
.contains("A selector is required when a collection contains multiple requests"));
}
#[test]
fn rejects_invalid_selector_in_non_interactive_mode() {
let workspace = TestWorkspace::new();
workspace.copy_fixture("non_interactive/multi-request.hen", "collection.hen");
let output = workspace.run_hen(["run", "collection.hen", "bogus", "--non-interactive"]);
assert_eq!(output.status_code, 2, "stderr: {}", output.stderr);
assert!(output.stdout.is_empty(), "stdout: {}", output.stdout);
assert!(output
.stderr
.contains("Selector must be an integer or 'all'"));
}
#[test]
fn rejects_missing_prompt_inputs_in_non_interactive_mode() {
let workspace = TestWorkspace::new();
workspace.copy_fixture(
"non_interactive/single-request-with-prompt.hen",
"collection.hen",
);
let output = workspace.run_hen(["run", "collection.hen", "--non-interactive"]);
assert_eq!(output.status_code, 2, "stderr: {}", output.stderr);
assert!(output.stdout.is_empty(), "stdout: {}", output.stdout);
assert!(output
.stderr
.contains("One or more required prompt inputs were not supplied"));
assert!(output.stderr.contains("Missing value for prompt 'item_id'"));
}
#[test]
fn rejects_missing_prompt_inputs_by_default() {
let workspace = TestWorkspace::new();
workspace.copy_fixture(
"non_interactive/single-request-with-prompt.hen",
"collection.hen",
);
let output = workspace.run_hen(["run", "collection.hen"]);
assert_eq!(output.status_code, 2, "stderr: {}", output.stderr);
assert!(output.stdout.is_empty(), "stdout: {}", output.stdout);
assert!(output
.stderr
.contains("One or more required prompt inputs were not supplied"));
assert!(output.stderr.contains("Missing value for prompt 'item_id'"));
}
#[test]
fn rejects_missing_prompt_inputs_in_multipart_file_paths() {
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
r#"Multipart Prompt
Ensures file-part prompts are rejected before execution.
---
Upload file
POST https://example.com/upload
~ file = @[[ file_path ]]
"#,
);
let output = workspace.run_hen(["run", "collection.hen"]);
assert_eq!(output.status_code, 2, "stderr: {}", output.stderr);
assert!(output.stdout.is_empty(), "stdout: {}", output.stdout);
assert!(output
.stderr
.contains("One or more required prompt inputs were not supplied"));
assert!(output.stderr.contains("Missing value for prompt 'file_path'"));
}
#[test]
fn fails_before_making_http_requests_when_later_request_has_missing_prompt() {
let hits = Arc::new(AtomicUsize::new(0));
let server_url = spawn_counting_http_server(Arc::clone(&hits));
let workspace = TestWorkspace::new();
workspace.write_file(
"collection.hen",
&format!(
r#"Prompt Preflight
Should reject unresolved prompt inputs before any request runs.
---
Ready request
GET {server_url}/ready
---
Blocked request
GET {server_url}/items/[[ item_id ]]
"#
),
);
let output = workspace.run_hen(["run", "collection.hen", "all"]);
assert_eq!(output.status_code, 2, "stderr: {}", output.stderr);
assert!(output.stdout.is_empty(), "stdout: {}", output.stdout);
assert!(output
.stderr
.contains("One or more required prompt inputs were not supplied"));
assert!(output.stderr.contains("Missing value for prompt 'item_id'"));
assert_eq!(hits.load(Ordering::SeqCst), 0, "no requests should be sent");
}
#[test]
fn rejects_ambiguous_directory_selection_in_non_interactive_mode() {
let workspace = TestWorkspace::new();
workspace.copy_fixture(
"non_interactive/single-request-with-prompt.hen",
"collections/one.hen",
);
workspace.copy_fixture("non_interactive/multi-request.hen", "collections/two.hen");
let output = workspace.run_hen(["run", "collections", "--non-interactive"]);
assert_eq!(output.status_code, 2, "stderr: {}", output.stderr);
assert!(output.stdout.is_empty(), "stdout: {}", output.stdout);
assert!(output
.stderr
.contains("cannot be resolved non-interactively"));
}
fn spawn_counting_http_server(hits: Arc<AtomicUsize>) -> String {
let listener = TcpListener::bind("127.0.0.1:0").expect("listener should bind");
let address = listener.local_addr().expect("address should be available");
thread::spawn(move || {
while let Ok((mut stream, _)) = listener.accept() {
hits.fetch_add(1, Ordering::SeqCst);
let mut buffer = [0_u8; 1024];
let _ = stream.read(&mut buffer);
let body = "{}";
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
);
stream
.write_all(response.as_bytes())
.expect("response should be written");
}
});
format!("http://{}", address)
}