use esi::{Configuration, Processor};
use fastly::{Request, Response};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[test]
fn test_eval_dca_none_parent_context() -> esi::Result<()> {
let input = r#"
<esi:assign name="pvar1" value="7"/>
<esi:assign name="pvar2" value="8"/>
<esi:eval src="http://example.com/frag1.html" dca="none"/>
<esi:vars>pvar1 = $(pvar1)
pvar2 = $(pvar2)
fvar = $(fvar)
</esi:vars>"#;
let dispatcher =
|_req: Request, _maxwait: Option<u32>| -> esi::Result<esi::PendingFragmentContent> {
Ok(esi::PendingFragmentContent::CompletedRequest(Box::new(
Response::from_body(
r#"
<esi:assign name="fvar" value="9"/>
<esi:assign name="pvar2" value="0"/>"#,
),
)))
};
let reader = std::io::BufReader::new(std::io::Cursor::new(input.as_bytes()));
let mut output = Vec::new();
let mut processor = Processor::new(None, Configuration::default());
processor.process_stream(reader, &mut output, Some(&dispatcher), None)?;
let result = String::from_utf8(output).unwrap();
assert_eq!(
result.trim(),
r#"pvar1 = 7
pvar2 = 0
fvar = 9"#,
"Fragment should execute in parent context, variables should be shared/overridden"
);
Ok(())
}
#[test]
fn test_eval_dca_esi_isolated_context() -> esi::Result<()> {
let input = r#"
<esi:assign name="pvar1" value="7"/>
<esi:assign name="pvar2" value="8"/>
<esi:eval src="http://example.com/frag1.html" dca="esi"/>
<esi:vars>pvar1 = $(pvar1)
pvar2 = $(pvar2)
fvar = $(fvar)
</esi:vars>"#;
let dispatcher =
|_req: Request, _maxwait: Option<u32>| -> esi::Result<esi::PendingFragmentContent> {
Ok(esi::PendingFragmentContent::CompletedRequest(Box::new(
Response::from_body(
r#"
<esi:assign name="fvar" value="9"/>
<esi:assign name="pvar2" value="0"/>"#,
),
)))
};
let reader = std::io::BufReader::new(std::io::Cursor::new(input.as_bytes()));
let mut output = Vec::new();
let mut processor = Processor::new(None, Configuration::default());
processor.process_stream(reader, &mut output, Some(&dispatcher), None)?;
let result = String::from_utf8(output).unwrap();
assert_eq!(
result.trim(),
r#"pvar1 = 7
pvar2 = 8
fvar ="#,
"Parent variables should remain unchanged, fragment variables should not leak"
);
Ok(())
}
#[test]
fn test_eval_dca_esi_with_output() -> esi::Result<()> {
let input = r#"
<esi:assign name="parent_var" value="'from_parent'"/>
<esi:eval src="http://example.com/fragment" dca="esi"/>
<esi:vars>After: $(fragment_var)</esi:vars>"#;
let dispatcher =
|_req: Request, _maxwait: Option<u32>| -> esi::Result<esi::PendingFragmentContent> {
Ok(esi::PendingFragmentContent::CompletedRequest(Box::new(
Response::from_body(
r#"
<esi:assign name="fragment_var" value="'from_fragment'"/>
<esi:vars>Output from fragment</esi:vars>"#,
),
)))
};
let reader = std::io::BufReader::new(std::io::Cursor::new(input.as_bytes()));
let mut output = Vec::new();
let mut processor = Processor::new(None, Configuration::default());
processor.process_stream(reader, &mut output, Some(&dispatcher), None)?;
let result = String::from_utf8(output).unwrap();
assert_eq!(
result.trim(),
"Output from fragment\nAfter:",
"Should output text from fragment, but fragment variables should not leak to parent"
);
Ok(())
}
#[test]
fn test_include_dca_none_no_processing() -> esi::Result<()> {
let input = r#"<esi:include src="http://example.com/fragment" dca="none"/>"#;
let dispatcher =
|_req: Request, _maxwait: Option<u32>| -> esi::Result<esi::PendingFragmentContent> {
Ok(esi::PendingFragmentContent::CompletedRequest(Box::new(
Response::from_body(
r#"<esi:assign name="x" value="42"/><esi:vars>X is $(x)</esi:vars>"#,
),
)))
};
let reader = std::io::BufReader::new(std::io::Cursor::new(input.as_bytes()));
let mut output = Vec::new();
let mut processor = Processor::new(None, Configuration::default());
processor.process_stream(reader, &mut output, Some(&dispatcher), None)?;
let result = String::from_utf8(output).unwrap();
assert_eq!(
result, r#"<esi:assign name="x" value="42"/><esi:vars>X is $(x)</esi:vars>"#,
"dca='none' should insert content verbatim without ESI processing"
);
Ok(())
}
#[test]
fn test_include_dca_esi_processes_content() -> esi::Result<()> {
let input = r#"<esi:include src="http://example.com/fragment" dca="esi"/>"#;
let dispatcher =
|_req: Request, _maxwait: Option<u32>| -> esi::Result<esi::PendingFragmentContent> {
Ok(esi::PendingFragmentContent::CompletedRequest(Box::new(
Response::from_body(
r#"<esi:assign name="y" value="99"/><esi:vars>Y is $(y)</esi:vars>"#,
),
)))
};
let reader = std::io::BufReader::new(std::io::Cursor::new(input.as_bytes()));
let mut output = Vec::new();
let mut processor = Processor::new(None, Configuration::default());
processor.process_stream(reader, &mut output, Some(&dispatcher), None)?;
let result = String::from_utf8(output).unwrap();
assert_eq!(result, "Y is 99", "dca='esi' should process content as ESI");
Ok(())
}
#[test]
fn test_include_dca_esi_isolates_namespace() -> esi::Result<()> {
let input = r#"<esi:include src="http://example.com/fragment" dca="esi"/><esi:vars>After include: $(shared_var)</esi:vars>"#;
let dispatcher =
|_req: Request, _maxwait: Option<u32>| -> esi::Result<esi::PendingFragmentContent> {
Ok(esi::PendingFragmentContent::CompletedRequest(Box::new(
Response::from_body(r#"<esi:assign name="shared_var" value="'shared'"/>"#),
)))
};
let reader = std::io::BufReader::new(std::io::Cursor::new(input.as_bytes()));
let mut output = Vec::new();
let mut processor = Processor::new(None, Configuration::default());
processor.process_stream(reader, &mut output, Some(&dispatcher), None)?;
let result = String::from_utf8(output).unwrap();
assert_eq!(
result, "After include: ",
"Include with dca='esi' must not leak variables to parent namespace"
);
Ok(())
}
#[test]
fn test_eval_vs_include_dca_difference() -> esi::Result<()> {
let input = r#"<esi:include src="http://example.com/raw"/><esi:eval src="http://example.com/processed"/>"#;
let calls = Arc::new(Mutex::new(HashMap::new()));
let calls_clone = calls.clone();
let dispatcher =
move |req: Request, _maxwait: Option<u32>| -> esi::Result<esi::PendingFragmentContent> {
let url = req.get_url().to_string();
calls_clone.lock().unwrap().insert(url.clone(), true);
let content = match url.as_str() {
"http://example.com/raw" => r#"<esi:vars>RAW</esi:vars>"#,
"http://example.com/processed" => r#"<esi:vars>PROCESSED</esi:vars>"#,
_ => "UNKNOWN",
};
Ok(esi::PendingFragmentContent::CompletedRequest(Box::new(
Response::from_body(content),
)))
};
let reader = std::io::BufReader::new(std::io::Cursor::new(input.as_bytes()));
let mut output = Vec::new();
let mut processor = Processor::new(None, Configuration::default());
processor.process_stream(reader, &mut output, Some(&dispatcher), None)?;
let result = String::from_utf8(output).unwrap();
assert_eq!(
result, r#"<esi:vars>RAW</esi:vars>PROCESSED"#,
"Include without dca should insert verbatim, eval should process as ESI"
);
let call_map = calls.lock().unwrap();
assert!(call_map.contains_key("http://example.com/raw"));
assert!(call_map.contains_key("http://example.com/processed"));
Ok(())
}
#[test]
fn test_eval_onerror_continue() -> esi::Result<()> {
let input = r#"Before<esi:eval src="http://example.com/fail" onerror="continue"/>After"#;
let dispatcher =
|_req: Request, _maxwait: Option<u32>| -> esi::Result<esi::PendingFragmentContent> {
Ok(esi::PendingFragmentContent::CompletedRequest(Box::new(
Response::from_status(500),
)))
};
let reader = std::io::BufReader::new(std::io::Cursor::new(input.as_bytes()));
let mut output = Vec::new();
let mut processor = Processor::new(None, Configuration::default());
processor.process_stream(reader, &mut output, Some(&dispatcher), None)?;
let result = String::from_utf8(output).unwrap();
assert_eq!(
result, "BeforeAfter",
"onerror='continue' should insert nothing on failure"
);
Ok(())
}
#[test]
fn test_eval_with_nested_esi() -> esi::Result<()> {
let input = r#"<esi:eval src="http://example.com/nested"/>"#;
let call_count = Arc::new(Mutex::new(0));
let call_count_clone = call_count.clone();
let dispatcher = move |req: Request,
_maxwait: Option<u32>|
-> esi::Result<esi::PendingFragmentContent> {
let url = req.get_url().to_string();
*call_count_clone.lock().unwrap() += 1;
let content = match url.as_str() {
"http://example.com/nested" => {
r#"<esi:choose><esi:when test="1 == 1">Chosen</esi:when><esi:otherwise>Not</esi:otherwise></esi:choose>"#
}
_ => "UNKNOWN",
};
Ok(esi::PendingFragmentContent::CompletedRequest(Box::new(
Response::from_body(content),
)))
};
let reader = std::io::BufReader::new(std::io::Cursor::new(input.as_bytes()));
let mut output = Vec::new();
let mut processor = Processor::new(None, Configuration::default());
processor.process_stream(reader, &mut output, Some(&dispatcher), None)?;
let result = String::from_utf8(output).unwrap();
assert_eq!(
result, "Chosen",
"eval should process nested ESI constructs"
);
assert_eq!(
*call_count.lock().unwrap(),
1,
"Should only call dispatcher once"
);
Ok(())
}
#[test]
fn test_nested_dca_esi_document_order() -> esi::Result<()> {
let input = r#"<html>
<body>
<header>
<esi:include src="http://example.com/header" dca="esi" />
</header>
<main>Main content</main>
</body>
</html>"#;
let dispatcher =
|req: Request, _maxwait: Option<u32>| -> esi::Result<esi::PendingFragmentContent> {
let url = req.get_url_str();
let content = if url.contains("/header") {
r#"<h1>Site Title</h1>
<nav>
<esi:include src="http://example.com/menu" dca="esi" />
</nav>"#
} else if url.contains("/menu") {
"<ul><li>Home</li><li>About</li></ul>"
} else {
""
};
Ok(esi::PendingFragmentContent::CompletedRequest(Box::new(
Response::from_body(content),
)))
};
let reader = std::io::BufReader::new(std::io::Cursor::new(input.as_bytes()));
let mut output = Vec::new();
let mut processor = Processor::new(None, Configuration::default());
processor.process_stream(reader, &mut output, Some(&dispatcher), None)?;
let result = String::from_utf8(output).unwrap();
let nav_start = result.find("<nav>").expect("<nav> should be present");
let menu_pos = result
.find("<ul><li>Home</li>")
.expect("menu content should be present");
let nav_end = result.find("</nav>").expect("</nav> should be present");
let main_pos = result.find("<main>").expect("<main> should be present");
assert!(
menu_pos > nav_start && menu_pos < nav_end,
"Menu must appear inside <nav>. Got:\n{result}"
);
assert!(
nav_end < main_pos,
"</nav> must appear before <main>. Got:\n{result}"
);
Ok(())
}
#[test]
fn test_triple_nested_dca_esi_document_order() -> esi::Result<()> {
let input =
r#"<div class="root"><esi:include src="http://example.com/level1" dca="esi" /></div>"#;
let dispatcher =
|req: Request, _maxwait: Option<u32>| -> esi::Result<esi::PendingFragmentContent> {
let url = req.get_url_str();
let content = if url.contains("/level1") {
r#"[L1-before]<esi:include src="http://example.com/level2" dca="esi" />[L1-after]"#
} else if url.contains("/level2") {
r#"[L2-before]<esi:include src="http://example.com/level3" dca="esi" />[L2-after]"#
} else if url.contains("/level3") {
"[L3-content]"
} else {
""
};
Ok(esi::PendingFragmentContent::CompletedRequest(Box::new(
Response::from_body(content),
)))
};
let reader = std::io::BufReader::new(std::io::Cursor::new(input.as_bytes()));
let mut output = Vec::new();
let mut processor = Processor::new(None, Configuration::default());
processor.process_stream(reader, &mut output, Some(&dispatcher), None)?;
let result = String::from_utf8(output).unwrap();
assert_eq!(
result, r#"<div class="root">[L1-before][L2-before][L3-content][L2-after][L1-after]</div>"#,
"Three-level nested dca='esi' must preserve document order"
);
Ok(())
}