use reqwest::Client;
use serde_json::json;
use std::time::{Duration, Instant};
use tokio::time::sleep;
const ADMIN_URL: &str = "http://127.0.0.1";
const TEST_TIMEOUT: Duration = Duration::from_secs(30);
fn get_test_ports() -> (u16, u16) {
use std::sync::atomic::{AtomicU16, Ordering};
static PORT_COUNTER: AtomicU16 = AtomicU16::new(18000);
let admin = PORT_COUNTER.fetch_add(2, Ordering::SeqCst);
let imposter = admin + 1;
(admin, imposter)
}
async fn start_rift_server(admin_port: u16) -> tokio::process::Child {
let child = tokio::process::Command::new("cargo")
.args([
"run",
"--package",
"rift-http-proxy",
"--",
"--port",
&admin_port.to_string(),
"--allow-injection",
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("Failed to start Rift server");
let client = Client::new();
for _ in 0..50 {
if client
.get(format!("{ADMIN_URL}:{admin_port}/"))
.timeout(Duration::from_millis(200))
.send()
.await
.is_ok()
{
return child;
}
sleep(Duration::from_millis(100)).await;
}
panic!("Rift server failed to start within timeout");
}
async fn create_imposter(client: &Client, admin_port: u16, config: serde_json::Value) -> u16 {
let response = client
.post(format!("{ADMIN_URL}:{admin_port}/imposters"))
.json(&config)
.send()
.await
.expect("Failed to create imposter");
assert!(
response.status().is_success(),
"Failed to create imposter: {}",
response.text().await.unwrap_or_default()
);
let body: serde_json::Value = response.json().await.expect("Failed to parse response");
body["port"].as_u64().expect("Missing port in response") as u16
}
async fn clear_imposters(client: &Client, admin_port: u16) {
let _ = client
.delete(format!("{ADMIN_URL}:{admin_port}/imposters"))
.send()
.await;
}
#[tokio::test]
#[ignore = "requires running server"]
async fn test_rift_flow_state_inmemory_basic() {
let (admin_port, imposter_port) = get_test_ports();
let mut server = start_rift_server(admin_port).await;
let client = Client::builder().timeout(TEST_TIMEOUT).build().unwrap();
let config = json!({
"port": imposter_port,
"protocol": "http",
"_rift": {
"flowState": {
"backend": "inmemory",
"ttlSeconds": 300
}
},
"stubs": [{
"predicates": [],
"responses": [{
"inject": "function(request, state) { state.counter = (state.counter || 0) + 1; return { statusCode: 200, body: 'Count: ' + state.counter }; }"
}]
}]
});
create_imposter(&client, admin_port, config).await;
for i in 1..=3 {
let response = client
.get(format!("{ADMIN_URL}:{imposter_port}/test"))
.send()
.await
.expect("Request failed");
assert_eq!(response.status(), 200);
let body = response.text().await.unwrap();
assert_eq!(body, format!("Count: {i}"));
}
clear_imposters(&client, admin_port).await;
server.kill().await.ok();
}
#[tokio::test]
#[ignore = "requires running server"]
async fn test_rift_flow_state_persistence_across_requests() {
let (admin_port, imposter_port) = get_test_ports();
let mut server = start_rift_server(admin_port).await;
let client = Client::builder().timeout(TEST_TIMEOUT).build().unwrap();
let config = json!({
"port": imposter_port,
"protocol": "http",
"_rift": {
"flowState": {
"backend": "inmemory"
}
},
"stubs": [
{
"predicates": [{"equals": {"method": "POST", "path": "/store"}}],
"responses": [{
"inject": "function(request, state) { var data = JSON.parse(request.body); state.stored = data.value; return { statusCode: 201, body: 'Stored' }; }"
}]
},
{
"predicates": [{"equals": {"method": "GET", "path": "/retrieve"}}],
"responses": [{
"inject": "function(request, state) { return { statusCode: 200, body: state.stored || 'empty' }; }"
}]
}
]
});
create_imposter(&client, admin_port, config).await;
let response = client
.get(format!("{ADMIN_URL}:{imposter_port}/retrieve"))
.send()
.await
.expect("Request failed");
assert_eq!(response.text().await.unwrap(), "empty");
let response = client
.post(format!("{ADMIN_URL}:{imposter_port}/store"))
.body(r#"{"value": "test-data"}"#)
.send()
.await
.expect("Request failed");
assert_eq!(response.status(), 201);
let response = client
.get(format!("{ADMIN_URL}:{imposter_port}/retrieve"))
.send()
.await
.expect("Request failed");
assert_eq!(response.text().await.unwrap(), "test-data");
clear_imposters(&client, admin_port).await;
server.kill().await.ok();
}
#[tokio::test]
#[ignore = "requires running server"]
async fn test_rift_fault_latency_100_percent() {
let (admin_port, imposter_port) = get_test_ports();
let mut server = start_rift_server(admin_port).await;
let client = Client::builder().timeout(TEST_TIMEOUT).build().unwrap();
let config = json!({
"port": imposter_port,
"protocol": "http",
"stubs": [{
"predicates": [],
"responses": [{
"is": {
"statusCode": 200,
"body": "delayed response"
},
"_rift": {
"fault": {
"latency": {
"probability": 1.0,
"ms": 200
}
}
}
}]
}]
});
create_imposter(&client, admin_port, config).await;
let start = Instant::now();
let response = client
.get(format!("{ADMIN_URL}:{imposter_port}/test"))
.send()
.await
.expect("Request failed");
let elapsed = start.elapsed();
assert_eq!(response.status(), 200);
assert_eq!(response.text().await.unwrap(), "delayed response");
assert!(
elapsed >= Duration::from_millis(180),
"Expected at least 180ms delay, got {elapsed:?}"
);
clear_imposters(&client, admin_port).await;
server.kill().await.ok();
}
#[tokio::test]
#[ignore = "requires running server"]
async fn test_rift_fault_latency_range() {
let (admin_port, imposter_port) = get_test_ports();
let mut server = start_rift_server(admin_port).await;
let client = Client::builder().timeout(TEST_TIMEOUT).build().unwrap();
let config = json!({
"port": imposter_port,
"protocol": "http",
"stubs": [{
"predicates": [],
"responses": [{
"is": {
"statusCode": 200,
"body": "ok"
},
"_rift": {
"fault": {
"latency": {
"probability": 1.0,
"minMs": 100,
"maxMs": 200
}
}
}
}]
}]
});
create_imposter(&client, admin_port, config).await;
let start = Instant::now();
let response = client
.get(format!("{ADMIN_URL}:{imposter_port}/test"))
.send()
.await
.expect("Request failed");
let elapsed = start.elapsed();
assert_eq!(response.status(), 200);
assert!(
elapsed >= Duration::from_millis(90),
"Expected at least 90ms delay, got {elapsed:?}"
);
assert!(
elapsed <= Duration::from_millis(300),
"Expected at most 300ms delay, got {elapsed:?}"
);
clear_imposters(&client, admin_port).await;
server.kill().await.ok();
}
#[tokio::test]
#[ignore = "requires running server"]
async fn test_rift_fault_error_100_percent() {
let (admin_port, imposter_port) = get_test_ports();
let mut server = start_rift_server(admin_port).await;
let client = Client::builder().timeout(TEST_TIMEOUT).build().unwrap();
let config = json!({
"port": imposter_port,
"protocol": "http",
"stubs": [{
"predicates": [],
"responses": [{
"is": {
"statusCode": 200,
"body": "normal response"
},
"_rift": {
"fault": {
"error": {
"probability": 1.0,
"status": 503,
"body": "Service Unavailable"
}
}
}
}]
}]
});
create_imposter(&client, admin_port, config).await;
let response = client
.get(format!("{ADMIN_URL}:{imposter_port}/test"))
.send()
.await
.expect("Request failed");
assert_eq!(response.status(), 503);
assert_eq!(response.text().await.unwrap(), "Service Unavailable");
clear_imposters(&client, admin_port).await;
server.kill().await.ok();
}
#[tokio::test]
#[ignore = "requires running server"]
async fn test_rift_fault_probabilistic() {
let (admin_port, imposter_port) = get_test_ports();
let mut server = start_rift_server(admin_port).await;
let client = Client::builder().timeout(TEST_TIMEOUT).build().unwrap();
let config = json!({
"port": imposter_port,
"protocol": "http",
"stubs": [{
"predicates": [],
"responses": [{
"is": {
"statusCode": 200,
"body": "success"
},
"_rift": {
"fault": {
"error": {
"probability": 0.5,
"status": 500,
"body": "error"
}
}
}
}]
}]
});
create_imposter(&client, admin_port, config).await;
let mut success_count = 0;
let mut error_count = 0;
for _ in 0..100 {
let response = client
.get(format!("{ADMIN_URL}:{imposter_port}/test"))
.send()
.await
.expect("Request failed");
if response.status() == 200 {
success_count += 1;
} else if response.status() == 500 {
error_count += 1;
}
}
assert!(
(25..=75).contains(&success_count),
"Expected roughly 50% success rate, got {success_count} successes and {error_count} errors"
);
clear_imposters(&client, admin_port).await;
server.kill().await.ok();
}
#[tokio::test]
#[ignore = "requires running server"]
async fn test_mountebank_behaviors_with_rift_fault() {
let (admin_port, imposter_port) = get_test_ports();
let mut server = start_rift_server(admin_port).await;
let client = Client::builder().timeout(TEST_TIMEOUT).build().unwrap();
let config = json!({
"port": imposter_port,
"protocol": "http",
"stubs": [{
"predicates": [{"equals": {"path": "/test"}}],
"responses": [{
"is": {
"statusCode": 200,
"headers": {"X-Custom": "header"},
"body": "response with both behaviors"
},
"_behaviors": {
"wait": 50
},
"_rift": {
"fault": {
"latency": {
"probability": 1.0,
"ms": 50
}
}
}
}]
}]
});
create_imposter(&client, admin_port, config).await;
let start = Instant::now();
let response = client
.get(format!("{ADMIN_URL}:{imposter_port}/test"))
.send()
.await
.expect("Request failed");
let elapsed = start.elapsed();
assert_eq!(response.status(), 200);
assert_eq!(
response
.headers()
.get("X-Custom")
.map(|v| v.to_str().unwrap()),
Some("header")
);
assert!(
elapsed >= Duration::from_millis(80),
"Expected at least 80ms combined delay, got {elapsed:?}"
);
clear_imposters(&client, admin_port).await;
server.kill().await.ok();
}
#[tokio::test]
#[ignore = "requires running server"]
async fn test_mountebank_predicates_with_rift_extensions() {
let (admin_port, imposter_port) = get_test_ports();
let mut server = start_rift_server(admin_port).await;
let client = Client::builder().timeout(TEST_TIMEOUT).build().unwrap();
let config = json!({
"port": imposter_port,
"protocol": "http",
"_rift": {
"flowState": {"backend": "inmemory"}
},
"stubs": [
{
"predicates": [
{"equals": {"method": "GET"}},
{"startsWith": {"path": "/api/"}}
],
"responses": [{
"is": {
"statusCode": 200,
"body": "API response"
},
"_rift": {
"fault": {
"latency": {"probability": 1.0, "ms": 10}
}
}
}]
},
{
"predicates": [{"equals": {"method": "POST"}}],
"responses": [{
"is": {
"statusCode": 201,
"body": "Created"
}
}]
}
]
});
create_imposter(&client, admin_port, config).await;
let start = Instant::now();
let response = client
.get(format!("{ADMIN_URL}:{imposter_port}/api/users"))
.send()
.await
.expect("Request failed");
let elapsed = start.elapsed();
assert_eq!(response.status(), 200);
assert_eq!(response.text().await.unwrap(), "API response");
assert!(elapsed >= Duration::from_millis(5));
let response = client
.post(format!("{ADMIN_URL}:{imposter_port}/create"))
.send()
.await
.expect("Request failed");
assert_eq!(response.status(), 201);
clear_imposters(&client, admin_port).await;
server.kill().await.ok();
}
#[tokio::test]
#[ignore = "requires running server"]
async fn test_response_cycling_with_rift_extensions() {
let (admin_port, imposter_port) = get_test_ports();
let mut server = start_rift_server(admin_port).await;
let client = Client::builder().timeout(TEST_TIMEOUT).build().unwrap();
let config = json!({
"port": imposter_port,
"protocol": "http",
"stubs": [{
"predicates": [],
"responses": [
{
"is": {"statusCode": 200, "body": "first"},
"_rift": {"fault": {"latency": {"probability": 1.0, "ms": 10}}}
},
{
"is": {"statusCode": 200, "body": "second"}
},
{
"is": {"statusCode": 200, "body": "third"},
"_rift": {"fault": {"latency": {"probability": 1.0, "ms": 10}}}
}
]
}]
});
create_imposter(&client, admin_port, config).await;
let bodies: Vec<String> = futures::future::join_all((0..6).map(|_| {
let c = client.clone();
let port = imposter_port;
async move {
c.get(format!("{ADMIN_URL}:{port}/test"))
.send()
.await
.unwrap()
.text()
.await
.unwrap()
}
}))
.await;
assert_eq!(
bodies,
vec!["first", "second", "third", "first", "second", "third"]
);
clear_imposters(&client, admin_port).await;
server.kill().await.ok();
}
#[tokio::test]
#[ignore = "requires running server"]
async fn test_default_response_with_rift_config() {
let (admin_port, imposter_port) = get_test_ports();
let mut server = start_rift_server(admin_port).await;
let client = Client::builder().timeout(TEST_TIMEOUT).build().unwrap();
let config = json!({
"port": imposter_port,
"protocol": "http",
"_rift": {
"flowState": {"backend": "inmemory"}
},
"defaultResponse": {
"statusCode": 404,
"body": "Not found"
},
"stubs": [{
"predicates": [{"equals": {"path": "/exists"}}],
"responses": [{
"is": {"statusCode": 200, "body": "Found"}
}]
}]
});
create_imposter(&client, admin_port, config).await;
let response = client
.get(format!("{ADMIN_URL}:{imposter_port}/exists"))
.send()
.await
.expect("Request failed");
assert_eq!(response.status(), 200);
assert_eq!(response.text().await.unwrap(), "Found");
let response = client
.get(format!("{ADMIN_URL}:{imposter_port}/nonexistent"))
.send()
.await
.expect("Request failed");
assert_eq!(response.status(), 404);
assert_eq!(response.text().await.unwrap(), "Not found");
clear_imposters(&client, admin_port).await;
server.kill().await.ok();
}