use esi::{Configuration, Processor};
use fastly::{Backend, 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(())
}
struct FlushTrackingWriter {
data: Vec<u8>,
flush_snapshots: Vec<Vec<u8>>,
}
impl FlushTrackingWriter {
fn new() -> Self {
Self {
data: Vec::new(),
flush_snapshots: Vec::new(),
}
}
}
impl std::io::Write for FlushTrackingWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.data.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
self.flush_snapshots.push(self.data.clone());
Ok(())
}
}
#[test]
fn test_streaming_flush_before_pending_include() -> esi::Result<()> {
let input = r#"<html><body>
<header>Header</header>
<esi:include src="http://example.com/slow-fragment" />
<footer>Footer</footer>
</body></html>"#;
let dispatcher =
|_req: Request, _maxwait: Option<u32>| -> esi::Result<esi::PendingFragmentContent> {
Ok(esi::PendingFragmentContent::CompletedRequest(Box::new(
Response::from_body("FRAGMENT"),
)))
};
let reader = std::io::BufReader::new(std::io::Cursor::new(input.as_bytes()));
let mut writer = FlushTrackingWriter::new();
let mut processor = Processor::new(None, Configuration::default());
processor.process_stream(reader, &mut writer, Some(&dispatcher), None)?;
let result = String::from_utf8(writer.data).unwrap();
assert!(
result.contains("<header>Header</header>"),
"Should contain header"
);
assert!(result.contains("FRAGMENT"), "Should contain fragment");
assert!(
result.contains("<footer>Footer</footer>"),
"Should contain footer"
);
assert!(
!writer.flush_snapshots.is_empty(),
"expected at least one flush during processing"
);
let first_flush = String::from_utf8_lossy(&writer.flush_snapshots[0]);
assert!(
first_flush.contains("<header>Header</header>"),
"First flush should contain content before the include. Got: {first_flush}"
);
Ok(())
}
#[test]
#[ignore]
fn test_nested_dca_esi_real_async_streaming() -> esi::Result<()> {
let input = r#"<h1>Title</h1>
<esi:include src="http://example.com/parent" dca="esi" />
<footer>End</footer>"#;
let dispatcher =
|req: Request, _maxwait: Option<u32>| -> esi::Result<esi::PendingFragmentContent> {
let url = req.get_url_str().to_string();
if url.contains("example.com/parent") {
Ok(esi::PendingFragmentContent::CompletedRequest(Box::new(
Response::from_body(
"<p>Fast content</p>\n<esi:include src=\"https://httpbin.org/delay/1\" />",
),
)))
} else {
let backend = Backend::builder("httpbin.org", "httpbin.org")
.enable_ssl()
.sni_hostname("httpbin.org")
.finish()
.map_err(|e| {
esi::ESIError::FragmentRequestError(format!(
"failed to create httpbin backend: {e}"
))
})?;
let pending = req.send_async(backend)?;
Ok(esi::PendingFragmentContent::PendingRequest(Box::new(
pending,
)))
}
};
let reader = std::io::BufReader::new(std::io::Cursor::new(input.as_bytes()));
let mut writer = FlushTrackingWriter::new();
let mut processor = Processor::new(None, Configuration::default());
processor.process_stream(reader, &mut writer, Some(&dispatcher), None)?;
let result = String::from_utf8(writer.data).unwrap();
assert!(result.contains("<h1>Title</h1>"));
assert!(result.contains("<p>Fast content</p>"));
assert!(result.contains("<footer>End</footer>"));
let has_incremental_flush = writer.flush_snapshots.iter().any(|snap| {
let s = String::from_utf8_lossy(snap);
s.contains("<p>Fast content</p>") && !s.contains("<footer>End</footer>")
});
assert!(
has_incremental_flush,
"fast content should be flushed before the footer; snapshots: {:?}",
writer
.flush_snapshots
.iter()
.map(|s| String::from_utf8_lossy(s).to_string())
.collect::<Vec<_>>()
);
Ok(())
}