use std::collections::HashMap;
use std::io;
use std::net::{Ipv4Addr, SocketAddr};
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::Duration;
use base64::Engine as _;
use futures_util::StreamExt as _;
use futures_util::future::BoxFuture;
use microvms_app::testing::{Answer, FakeControlPlane, microvm_response};
use microvms_core::control::transport::Transport;
use microvms_core::control::{ControlPlane, SystemClock, WaitOpts};
use microvms_core::error::{Error, ErrorKind, WireKind};
use microvms_core::prelude::*;
use microvms_core::region::Region;
use microvms_core::session::{
ChunkSource, DEFAULT_AGENT_PORT, DEFAULT_REFRESH_AFTER, ExecEvent, HttpBackend, HttpRequest,
HttpResponse, OpenStream, PROXY_AUTH_HEADER, PROXY_PORT_HEADER, ProxyAuth, ProxyToken, Session,
StreamOptions, TokenMinter,
};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use turmoil::Sim;
use turmoil::net::{TcpListener, TcpStream};
const PORT: u16 = DEFAULT_AGENT_PORT;
const PROXY: &str = "endpoint-proxy";
const AGENT_TOKEN: &str = "sim-agent-token-0123456789";
const EXEC_ID: &str = "x-turmoil";
struct SimListener {
inner: TcpListener,
accepted: Arc<AtomicUsize>,
}
impl SimListener {
async fn accept(&mut self) -> io::Result<(TcpStream, SocketAddr)> {
let pair = self.inner.accept().await?;
self.accepted.fetch_add(1, Ordering::Relaxed);
Ok(pair)
}
}
#[derive(Clone)]
enum Serve {
Json(u16, String),
CutAfter(Vec<String>),
StreamThen(Vec<String>),
}
#[derive(Default)]
struct Script {
serves: Mutex<std::collections::VecDeque<Serve>>,
seen: Mutex<Vec<Recorded>>,
refused: AtomicU64,
expired: AtomicU64,
minted: Mutex<HashMap<String, Duration>>,
oldest_presented_secs: AtomicU64,
}
#[derive(Clone, Debug)]
struct Recorded {
method: String,
path: String,
headers: HashMap<String, String>,
}
impl Recorded {
fn header(&self, name: &str) -> Option<&str> {
self.headers
.get(&name.to_ascii_lowercase())
.map(String::as_str)
}
fn offset(&self) -> Option<u64> {
self.path
.split("offset=")
.nth(1)
.and_then(|rest| rest.split('&').next())
.and_then(|value| value.parse().ok())
}
}
impl Script {
fn with(serves: impl IntoIterator<Item = Serve>) -> Arc<Self> {
Arc::new(Self {
serves: Mutex::new(serves.into_iter().collect()),
..Self::default()
})
}
fn requests(&self) -> Vec<Recorded> {
self.seen
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
}
fn record_mint(&self, value: String) {
self.minted
.lock()
.unwrap_or_else(PoisonError::into_inner)
.insert(value, turmoil::sim_elapsed().unwrap_or_default());
}
fn judge(&self, value: &str) -> Verdict {
let minted = self.minted.lock().unwrap_or_else(PoisonError::into_inner);
if minted.is_empty() {
return Verdict::Valid;
}
let Some(minted_at) = minted.get(value) else {
return Verdict::Unknown;
};
let now = turmoil::sim_elapsed().unwrap_or_default();
let age = now.saturating_sub(*minted_at);
self.oldest_presented_secs
.fetch_max(age.as_secs(), Ordering::SeqCst);
if age >= CEILING {
Verdict::Expired
} else {
Verdict::Valid
}
}
fn oldest_presented(&self) -> Duration {
Duration::from_secs(self.oldest_presented_secs.load(Ordering::SeqCst))
}
}
#[derive(Debug, Eq, PartialEq)]
enum Verdict {
Valid,
Expired,
Unknown,
}
const CEILING: Duration = Duration::from_secs(60 * 60);
fn spawn_proxy(sim: &mut Sim<'_>, script: Arc<Script>) -> Arc<AtomicUsize> {
let accepted = Arc::new(AtomicUsize::new(0));
let counter = Arc::clone(&accepted);
sim.host(PROXY, move || {
let script = Arc::clone(&script);
let accepted = Arc::clone(&counter);
async move {
let inner = TcpListener::bind((Ipv4Addr::UNSPECIFIED, PORT)).await?;
let mut listener = SimListener { inner, accepted };
loop {
let (stream, _) = listener.accept().await?;
let script = Arc::clone(&script);
tokio::spawn(async move {
let _ = handle(stream, script).await;
});
}
}
});
accepted
}
async fn handle(mut stream: TcpStream, script: Arc<Script>) -> io::Result<()> {
let Some(recorded) = read_head(&mut stream).await? else {
return Ok(());
};
script
.seen
.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(recorded.clone());
let auth = recorded.header(PROXY_AUTH_HEADER);
let port = recorded.header(PROXY_PORT_HEADER);
if auth.is_none() || port.is_none() {
script.refused.fetch_add(1, Ordering::SeqCst);
return answer_json(
&mut stream,
403,
r#"{"message":"missing proxy credentials"}"#,
)
.await;
}
if port != Some(&PORT.to_string()) {
script.refused.fetch_add(1, Ordering::SeqCst);
return answer_json(&mut stream, 403, r#"{"message":"port not allowed"}"#).await;
}
match script.judge(auth.unwrap_or_default()) {
Verdict::Valid => {}
Verdict::Expired => {
script.expired.fetch_add(1, Ordering::SeqCst);
return answer_json(&mut stream, 403, r#"{"message":"token expired"}"#).await;
}
Verdict::Unknown => {
script.refused.fetch_add(1, Ordering::SeqCst);
return answer_json(&mut stream, 403, r#"{"message":"unknown token"}"#).await;
}
}
let serve = script
.serves
.lock()
.unwrap_or_else(PoisonError::into_inner)
.pop_front();
match serve {
Some(Serve::Json(status, body)) => answer_json(&mut stream, status, &body).await,
Some(Serve::CutAfter(frames)) => {
answer_sse(&mut stream, &frames, false).await?;
Ok(())
}
Some(Serve::StreamThen(frames)) => answer_sse(&mut stream, &frames, true).await,
None => answer_json(&mut stream, 500, r#"{"message":"script exhausted"}"#).await,
}
}
async fn read_head(stream: &mut TcpStream) -> io::Result<Option<Recorded>> {
let mut buf = Vec::new();
let head_end = loop {
if let Some(idx) = find(&buf, b"\r\n\r\n") {
break idx + 4;
}
let mut chunk = [0u8; 4096];
let n = stream.read(&mut chunk).await?;
if n == 0 {
return Ok(None);
}
buf.extend_from_slice(&chunk[..n]);
};
let head = String::from_utf8_lossy(&buf[..head_end]).into_owned();
let mut lines = head.lines();
let request_line = lines.next().unwrap_or_default();
let mut parts = request_line.split_whitespace();
let method = parts.next().unwrap_or_default().to_string();
let path = parts.next().unwrap_or_default().to_string();
let mut headers = HashMap::new();
let mut declared = 0usize;
for line in lines.take_while(|line| !line.is_empty()) {
if let Some((name, value)) = line.split_once(':') {
let name = name.trim().to_ascii_lowercase();
let value = value.trim().to_string();
if name == "content-length" {
declared = value.parse().unwrap_or(0);
}
headers.insert(name, value);
}
}
let mut have = buf.len() - head_end;
while have < declared {
let mut chunk = [0u8; 4096];
let n = stream.read(&mut chunk).await?;
if n == 0 {
break;
}
have += n;
}
Ok(Some(Recorded {
method,
path,
headers,
}))
}
async fn answer_json(stream: &mut TcpStream, status: u16, body: &str) -> io::Result<()> {
let response = format!(
"HTTP/1.1 {status} X\r\ncontent-type: application/json\r\n\
content-length: {}\r\nconnection: close\r\n\r\n{body}",
body.len()
);
stream.write_all(response.as_bytes()).await?;
stream.flush().await
}
async fn answer_sse(stream: &mut TcpStream, frames: &[String], graceful: bool) -> io::Result<()> {
let head = "HTTP/1.1 200 OK\r\ncontent-type: text/event-stream\r\n\
cache-control: no-cache\r\nx-accel-buffering: no\r\n\
transfer-encoding: chunked\r\n\r\n";
stream.write_all(head.as_bytes()).await?;
for frame in frames {
stream
.write_all(format!("{:x}\r\n{frame}\r\n", frame.len()).as_bytes())
.await?;
stream.flush().await?;
}
if graceful {
stream.write_all(b"0\r\n\r\n").await?;
stream.flush().await?;
}
Ok(())
}
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack
.windows(needle.len())
.position(|window| window == needle)
}
struct SimBackend;
fn wire_error(request: &HttpRequest, err: &io::Error) -> Error {
Error::wire(
WireKind::Transport,
format!(
"{} {} failed on the simulated wire: {err}",
request.method, request.path
),
)
}
#[allow(
clippy::type_complexity,
reason = "a test harness's one-shot parse result; naming a struct for it would put \
a fifth type between the reader and the five values it is about to assert on"
)]
async fn read_response_head(
stream: &mut TcpStream,
) -> io::Result<(u16, HashMap<String, String>, Vec<u8>, Option<usize>, bool)> {
let mut buf = Vec::new();
let head_end = loop {
if let Some(idx) = find(&buf, b"\r\n\r\n") {
break idx + 4;
}
let mut chunk = [0u8; 4096];
let n = stream.read(&mut chunk).await?;
if n == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"the connection closed before a response head",
));
}
buf.extend_from_slice(&chunk[..n]);
};
let head = String::from_utf8_lossy(&buf[..head_end]).into_owned();
let mut lines = head.lines();
let status = lines
.next()
.and_then(|line| line.split_whitespace().nth(1))
.and_then(|code| code.parse::<u16>().ok())
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "unparseable status line"))?;
let mut headers = HashMap::new();
for line in lines.take_while(|line| !line.is_empty()) {
if let Some((name, value)) = line.split_once(':') {
headers.insert(name.trim().to_ascii_lowercase(), value.trim().to_string());
}
}
let declared = headers
.get("content-length")
.and_then(|value| value.parse::<usize>().ok());
let chunked = headers
.get("transfer-encoding")
.is_some_and(|value| value.contains("chunked"));
Ok((status, headers, buf[head_end..].to_vec(), declared, chunked))
}
impl HttpBackend for SimBackend {
fn send(&self, request: HttpRequest) -> BoxFuture<'_, Result<HttpResponse, Error>> {
Box::pin(async move {
let mut stream = TcpStream::connect((PROXY, PORT))
.await
.map_err(|err| wire_error(&request, &err))?;
let mut head = format!(
"{} {} HTTP/1.1\r\nhost: {PROXY}\r\n",
request.method, request.path
);
for (name, value) in &request.headers {
head.push_str(&format!("{name}: {value}\r\n"));
}
head.push_str(&format!("content-length: {}\r\n\r\n", request.body.len()));
stream
.write_all(head.as_bytes())
.await
.map_err(|err| wire_error(&request, &err))?;
if !request.body.is_empty() {
stream
.write_all(&request.body)
.await
.map_err(|err| wire_error(&request, &err))?;
}
stream
.flush()
.await
.map_err(|err| wire_error(&request, &err))?;
let (status, headers, mut body, declared, _) = read_response_head(&mut stream)
.await
.map_err(|err| wire_error(&request, &err))?;
let want = declared.unwrap_or(0);
while body.len() < want {
let mut chunk = [0u8; 4096];
let n = stream
.read(&mut chunk)
.await
.map_err(|err| wire_error(&request, &err))?;
if n == 0 {
break;
}
body.extend_from_slice(&chunk[..n]);
}
Ok(HttpResponse {
status,
headers,
body,
})
})
}
fn open_stream(
&self,
request: HttpRequest,
idle_timeout: Duration,
) -> BoxFuture<'_, Result<OpenStream, Error>> {
Box::pin(async move {
let mut stream = TcpStream::connect((PROXY, PORT))
.await
.map_err(|err| wire_error(&request, &err))?;
let mut head = format!(
"{} {} HTTP/1.1\r\nhost: {PROXY}\r\naccept: text/event-stream\r\n",
request.method, request.path
);
for (name, value) in &request.headers {
head.push_str(&format!("{name}: {value}\r\n"));
}
head.push_str("\r\n");
stream
.write_all(head.as_bytes())
.await
.map_err(|err| wire_error(&request, &err))?;
stream
.flush()
.await
.map_err(|err| wire_error(&request, &err))?;
let (status, headers, leftover, declared, chunked) = read_response_head(&mut stream)
.await
.map_err(|err| wire_error(&request, &err))?;
if !(200..300).contains(&status) {
let mut body = leftover;
let want = declared.unwrap_or(0);
while body.len() < want {
let mut chunk = [0u8; 4096];
let n = stream
.read(&mut chunk)
.await
.map_err(|err| wire_error(&request, &err))?;
if n == 0 {
break;
}
body.extend_from_slice(&chunk[..n]);
}
return Ok((
HttpResponse {
status,
headers,
body,
},
Box::new(NoChunks) as Box<dyn ChunkSource>,
));
}
let source = SimChunks {
stream: Some(stream),
wire: leftover,
chunk_left: 0,
chunked,
idle_timeout,
};
Ok((
HttpResponse {
status,
headers,
body: Vec::new(),
},
Box::new(source) as Box<dyn ChunkSource>,
))
})
}
}
struct NoChunks;
impl ChunkSource for NoChunks {
fn next_chunk(&mut self) -> BoxFuture<'_, Result<Option<Vec<u8>>, Error>> {
Box::pin(async { Ok(None) })
}
}
struct SimChunks {
stream: Option<TcpStream>,
wire: Vec<u8>,
chunk_left: usize,
chunked: bool,
idle_timeout: Duration,
}
impl SimChunks {
fn decode(&mut self) -> (Vec<u8>, bool) {
if !self.chunked {
return (std::mem::take(&mut self.wire), false);
}
let mut out = Vec::new();
loop {
if self.chunk_left == 0 {
if self.wire.starts_with(b"\r\n") {
self.wire.drain(..2);
continue;
}
let Some(idx) = find(&self.wire, b"\r\n") else {
return (out, false);
};
let header = String::from_utf8_lossy(&self.wire[..idx]).into_owned();
let Ok(size) = usize::from_str_radix(header.trim(), 16) else {
return (out, false);
};
self.wire.drain(..idx + 2);
if size == 0 {
return (out, true);
}
self.chunk_left = size;
}
let take = self.chunk_left.min(self.wire.len());
if take == 0 {
return (out, false);
}
out.extend(self.wire.drain(..take));
self.chunk_left -= take;
}
}
}
impl ChunkSource for SimChunks {
fn next_chunk(&mut self) -> BoxFuture<'_, Result<Option<Vec<u8>>, Error>> {
Box::pin(async move {
loop {
let (decoded, ended) = self.decode();
if !decoded.is_empty() {
return Ok(Some(decoded));
}
if ended {
self.stream = None;
return Ok(None);
}
let Some(stream) = self.stream.as_mut() else {
return Ok(None);
};
let mut chunk = [0u8; 4096];
match tokio::time::timeout(self.idle_timeout, stream.read(&mut chunk)).await {
Ok(Ok(0)) | Ok(Err(_)) => {
self.stream = None;
return Ok(None);
}
Ok(Ok(n)) => self.wire.extend_from_slice(&chunk[..n]),
Err(_) => {
self.stream = None;
return Err(Error::wire(
WireKind::Transport,
"the simulated stream went silent past the idle timeout",
));
}
}
}
})
}
}
struct SimMinter {
script: Arc<Script>,
issued: AtomicU64,
failures: AtomicU64,
}
impl SimMinter {
fn new(script: Arc<Script>) -> Self {
Self {
script,
issued: AtomicU64::new(0),
failures: AtomicU64::new(0),
}
}
fn failing(script: Arc<Script>, times: u64) -> Self {
Self {
script,
issued: AtomicU64::new(0),
failures: AtomicU64::new(times),
}
}
}
impl TokenMinter for SimMinter {
fn mint(&self) -> BoxFuture<'_, Result<ProxyToken, Error>> {
Box::pin(async move {
if self
.failures
.try_update(Ordering::SeqCst, Ordering::SeqCst, |left| {
left.checked_sub(1)
})
.is_ok()
{
return Err(Error::wire(
WireKind::AuthTokenMint,
"ThrottlingException from CreateMicrovmAuthToken",
));
}
let nth = self.issued.fetch_add(1, Ordering::SeqCst);
let value = format!("jwe-scoped-{nth}");
self.script.record_mint(value.clone());
Ok(ProxyToken::from_pairs([(PROXY_AUTH_HEADER, value)]))
})
}
}
fn sim(seed: u64) -> Sim<'static> {
turmoil::Builder::new()
.rng_seed(seed)
.enable_tokio_io()
.simulation_duration(Duration::from_secs(120))
.build()
}
fn long_sim(seed: u64) -> Sim<'static> {
sim_for(seed, Duration::from_secs(90 * 60))
}
fn sim_for(seed: u64, duration: Duration) -> Sim<'static> {
turmoil::Builder::new()
.rng_seed(seed)
.enable_tokio_io()
.simulation_duration(duration)
.tick_duration(Duration::from_millis(500))
.build()
}
fn session(minter: Arc<dyn TokenMinter>) -> Session {
Session::builder(format!("http://{PROXY}:{PORT}"), AGENT_TOKEN)
.with_backend(Arc::new(SimBackend))
.with_minter(minter)
.build()
.expect("the session builds")
}
fn session_with_auth(auth: Arc<ProxyAuth>) -> Session {
Session::builder(format!("http://{PROXY}:{PORT}"), AGENT_TOKEN)
.with_backend(Arc::new(SimBackend))
.with_proxy_auth(auth)
.build()
.expect("the session builds")
}
fn output_frame(offset: u64, bytes: &[u8]) -> String {
let payload = protocol::exec::OutputEvent {
offset,
stream: protocol::exec::StreamKind::Stdout,
output: base64::engine::general_purpose::STANDARD.encode(bytes),
};
format!(
"event: {}\ndata: {}\n\n",
protocol::exec::EVENT_OUTPUT,
serde_json::to_string(&payload).expect("serializes")
)
}
fn exit_frame(total: u64) -> String {
let payload = protocol::exec::ExitEvent {
exit_code: Some(0),
signal: None,
timed_out: false,
truncated: false,
writers_may_be_alive: false,
offset: total,
};
format!(
"event: {}\ndata: {}\n\n",
protocol::exec::EVENT_EXIT,
serde_json::to_string(&payload).expect("serializes")
)
}
fn health_json(bootstrapped: bool) -> String {
serde_json::to_string(&protocol::health::Health {
version: std::borrow::Cow::Borrowed("0.1.0"),
bootstrapped,
disk: None,
identity_degraded: false,
identity_repaired: true,
busy: false,
execs: 0,
hooks: Vec::new(),
hooks_dropped: 0,
identity_steps: Vec::new(),
image_env_keys: None,
})
.expect("serializes")
}
async fn drain_stream(
session: &Session,
options: StreamOptions,
) -> Result<(Vec<u8>, Vec<ExecEvent>), Error> {
let handle = session.exec(EXEC_ID);
let mut bytes = Vec::new();
let mut events = Vec::new();
let mut stream = std::pin::pin!(handle.stream_with(options));
while let Some(item) = stream.next().await {
let event = item?;
if let ExecEvent::Output { data, .. } = &event {
bytes.extend_from_slice(data);
}
events.push(event);
}
Ok((bytes, events))
}
#[test]
fn the_simulated_proxy_answers_a_request_the_real_session_built() -> turmoil::Result {
let script = Script::with([Serve::Json(200, health_json(true))]);
let mut sim = sim(0x5EED_0201);
spawn_proxy(&mut sim, Arc::clone(&script));
let minter = Arc::new(SimMinter::new(Arc::clone(&script)));
sim.client("harness", async move {
let session = session(minter);
let health = session.health().await?;
assert!(health.bootstrapped, "the proxy's body did not survive");
Ok(())
});
sim.run()?;
assert_eq!(script.requests().len(), 1);
assert_eq!(script.refused.load(Ordering::SeqCst), 0);
Ok(())
}
#[test]
fn both_proxy_headers_reach_the_proxy_on_every_request() -> turmoil::Result {
let script = Script::with([
Serve::Json(200, health_json(true)),
Serve::Json(
200,
serde_json::to_string(&protocol::exec::PollResponse {
exec_id: EXEC_ID.into(),
phase: protocol::exec::Phase::Running,
result: None,
})
.expect("serializes"),
),
Serve::StreamThen(vec![output_frame(0, b"hi\n"), exit_frame(3)]),
]);
let mut sim = sim(0x5EED_0202);
spawn_proxy(&mut sim, Arc::clone(&script));
let minter = Arc::new(SimMinter::new(Arc::clone(&script)));
sim.client("harness", async move {
let session = session(minter);
session.health().await?;
session.exec(EXEC_ID).poll().await?;
let (bytes, _) = drain_stream(&session, StreamOptions::default()).await?;
assert_eq!(bytes, b"hi\n");
Ok(())
});
sim.run()?;
let seen = script.requests();
assert_eq!(seen.len(), 3, "not every route was exercised: {seen:#?}");
for request in &seen {
assert_eq!(
request.header(PROXY_AUTH_HEADER),
Some("jwe-scoped-0"),
"{} {} arrived without the proxy auth header",
request.method,
request.path
);
assert_eq!(
request.header(PROXY_PORT_HEADER),
Some("9000"),
"{} {} arrived without the proxy port header, which the proxy refuses as \
if the token were bad",
request.method,
request.path
);
}
assert_eq!(
script.refused.load(Ordering::SeqCst),
0,
"the proxy refused a request, so a header was missing or wrong"
);
Ok(())
}
#[test]
fn the_proxy_refuses_a_request_that_omits_the_port_header() -> turmoil::Result {
let script = Script::with([Serve::Json(200, health_json(true))]);
let mut sim = sim(0x5EED_0203);
spawn_proxy(&mut sim, Arc::clone(&script));
sim.client("harness", async move {
let backend = SimBackend;
let mut request = HttpRequest::new("GET", "/v1/health".to_string());
request
.headers
.push((PROXY_AUTH_HEADER.to_string(), "jwe-scoped-0".to_string()));
let response = backend.send(request).await?;
assert_eq!(
response.status, 403,
"the proxy accepted a request with no port header, so the TRAP-7 guard \
above cannot fail"
);
Ok(())
});
sim.run()?;
assert_eq!(script.refused.load(Ordering::SeqCst), 1);
Ok(())
}
#[test]
fn a_mid_stream_disconnect_reconnects_at_the_cursor_and_loses_nothing() -> turmoil::Result {
let script = Script::with([
Serve::CutAfter(vec![output_frame(0, b"AAAA\n"), output_frame(5, b"BBBB\n")]),
Serve::StreamThen(vec![output_frame(10, b"CCCC\n"), exit_frame(15)]),
]);
let mut sim = sim(0x5EED_0204);
let accepted = spawn_proxy(&mut sim, Arc::clone(&script));
let minter = Arc::new(SimMinter::new(Arc::clone(&script)));
sim.client("harness", async move {
let session = session(minter);
let (bytes, events) = drain_stream(&session, StreamOptions::default()).await?;
assert_eq!(
String::from_utf8_lossy(&bytes),
"AAAA\nBBBB\nCCCC\n",
"the two attaches did not reconstruct the output, so the reconnect \
duplicated or lost bytes at the seam"
);
assert!(
matches!(events.last(), Some(ExecEvent::Exit(exit)) if exit.offset == 15),
"the stream did not end on the terminal event: {events:#?}"
);
Ok(())
});
sim.run()?;
let seen = script.requests();
assert_eq!(
seen.len(),
2,
"the cut did not produce exactly one reconnect: {seen:#?}"
);
assert_eq!(
seen[0].offset(),
Some(0),
"the first attach must start at 0"
);
assert_eq!(
seen[1].offset(),
Some(10),
"the reconnect asked for byte {:?} rather than 10, so the cursor did not \
follow what was delivered",
seen[1].offset()
);
assert_eq!(
accepted.load(Ordering::Relaxed),
2,
"the reconnect did not open a fresh connection"
);
Ok(())
}
#[test]
fn a_gap_moves_the_cursor_past_bytes_that_are_gone() -> turmoil::Result {
let gap = format!(
"event: {}\ndata: {}\n\n",
protocol::exec::EVENT_GAP,
serde_json::to_string(&protocol::exec::GapEvent { from: 2, to: 900 }).expect("serializes")
);
let script = Script::with([
Serve::CutAfter(vec![output_frame(0, b"AA"), gap]),
Serve::StreamThen(vec![output_frame(900, b"ZZ"), exit_frame(902)]),
]);
let mut sim = sim(0x5EED_0205);
spawn_proxy(&mut sim, Arc::clone(&script));
let minter = Arc::new(SimMinter::new(Arc::clone(&script)));
sim.client("harness", async move {
let session = session(minter);
let (bytes, events) = drain_stream(&session, StreamOptions::default()).await?;
assert_eq!(bytes, b"AAZZ");
assert!(
events
.iter()
.any(|event| matches!(event, ExecEvent::Gap { from: 2, to: 900 })),
"the gap was swallowed, so a truncated log reads as a complete one: \
{events:#?}"
);
Ok(())
});
sim.run()?;
let seen = script.requests();
assert_eq!(
seen[1].offset(),
Some(900),
"the reconnect asked for bytes the daemon had already evicted"
);
Ok(())
}
#[test]
fn a_session_that_outlives_the_token_ceiling_mints_inside_the_request_path() -> turmoil::Result {
let script = Script::with([
Serve::Json(200, health_json(true)),
Serve::Json(200, health_json(true)),
]);
let mut sim = long_sim(0x5EED_0206);
spawn_proxy(&mut sim, Arc::clone(&script));
let minter = Arc::new(SimMinter::new(Arc::clone(&script)));
let auth = Arc::new(ProxyAuth::new(minter, PORT));
let observed = Arc::clone(&auth);
sim.client("harness", async move {
let session = session_with_auth(auth);
session.health().await?;
tokio::time::sleep(Duration::from_secs(70 * 60)).await;
let health = session.health().await?;
assert!(
health.bootstrapped,
"the request after the token boundary failed"
);
Ok(())
});
sim.run()?;
assert_eq!(
observed.mint_count(),
2,
"the session did not re-mint across the {}s ceiling, so a long trial would \
die at minute sixty",
Duration::from_secs(60 * 60).as_secs()
);
assert_eq!(
script.expired.load(Ordering::SeqCst),
0,
"a request went out carrying an expired token; the mint must happen before \
the boundary rather than after a rejection"
);
assert_eq!(script.refused.load(Ordering::SeqCst), 0);
Ok(())
}
#[test]
fn a_long_trial_never_presents_a_token_near_its_ceiling() -> turmoil::Result {
let hours = Duration::from_secs(3 * 60 * 60);
let serves: Vec<Serve> = (0..18)
.map(|_| Serve::Json(200, health_json(true)))
.collect();
let script = Script::with(serves);
let mut sim = sim_for(0x5EED_0207, hours + Duration::from_secs(60));
spawn_proxy(&mut sim, Arc::clone(&script));
let minter = Arc::new(SimMinter::new(Arc::clone(&script)));
let auth = Arc::new(ProxyAuth::new(minter, PORT));
let observed = Arc::clone(&auth);
sim.client("harness", async move {
let session = session_with_auth(auth);
for _ in 0..18 {
let health = session.health().await?;
assert!(health.bootstrapped);
tokio::time::sleep(Duration::from_secs(10 * 60)).await;
}
Ok(())
});
sim.run()?;
let oldest = script.oldest_presented();
assert!(
CEILING - oldest >= CEILING / 2,
"a request presented a token {}s old, leaving only {}s of its {}s life: the \
refresh interval is too close to the ceiling, so a token can expire between \
building the headers and the proxy reading them",
oldest.as_secs(),
(CEILING - oldest).as_secs(),
CEILING.as_secs()
);
assert_eq!(
script.expired.load(Ordering::SeqCst),
0,
"the client presented a fully expired token, which the proxy answers with a 403 \
that reads like a wrong agent token"
);
assert_eq!(script.refused.load(Ordering::SeqCst), 0);
assert_eq!(script.requests().len(), 18);
let mints = observed.mint_count();
assert!(
(3..=7).contains(&mints),
"three hours at a thirty-minute refresh window should be four to six mints, \
got {mints}"
);
Ok(())
}
#[test]
fn the_refresh_happens_on_schedule_and_not_in_reaction_to_a_rejection() -> turmoil::Result {
let script = Script::with([
Serve::Json(200, health_json(true)),
Serve::Json(200, health_json(true)),
]);
let mut sim = long_sim(0x5EED_0207);
spawn_proxy(&mut sim, Arc::clone(&script));
let minter = Arc::new(SimMinter::new(Arc::clone(&script)));
let auth = Arc::new(ProxyAuth::new(minter, PORT));
let observed = Arc::clone(&auth);
sim.client("harness", async move {
let session = session_with_auth(auth);
session.health().await?;
tokio::time::sleep(DEFAULT_REFRESH_AFTER + Duration::from_secs(60)).await;
session.health().await?;
Ok(())
});
sim.run()?;
assert_eq!(
observed.mint_count(),
2,
"no mint happened inside the ceiling, so the refresh is reactive rather than \
scheduled and a token can expire in flight"
);
assert_eq!(
script.expired.load(Ordering::SeqCst),
0,
"the scheduled refresh should never have presented a stale token"
);
Ok(())
}
#[test]
fn a_throttled_mint_is_retryable_and_the_second_attempt_succeeds() -> turmoil::Result {
let script = Script::with([Serve::Json(200, health_json(true))]);
let mut sim = sim(0x5EED_0208);
spawn_proxy(&mut sim, Arc::clone(&script));
let minter = Arc::new(SimMinter::failing(Arc::clone(&script), 1));
let auth = Arc::new(ProxyAuth::new(minter, PORT));
let observed = Arc::clone(&auth);
let seen_after_failure = Arc::new(AtomicUsize::new(0));
let recorded = Arc::clone(&seen_after_failure);
let script_for_client = Arc::clone(&script);
sim.client("harness", async move {
let session = session_with_auth(auth);
let err = session
.health()
.await
.expect_err("the first mint is throttled");
assert!(
err.retryable(),
"a throttled mint was reported as fatal, which kills a healthy trial: {err}"
);
assert_eq!(err.wire_kind(), Some(WireKind::AuthTokenMint));
recorded.store(script_for_client.requests().len(), Ordering::SeqCst);
let health = session.health().await?;
assert!(health.bootstrapped, "the retry did not land");
Ok(())
});
sim.run()?;
assert_eq!(
seen_after_failure.load(Ordering::SeqCst),
0,
"a request reached the proxy without a token, so the mint failure was not \
raised before the request was built"
);
assert_eq!(observed.mint_count(), 1, "the failed mint was counted");
assert_eq!(script.requests().len(), 1);
assert_eq!(script.refused.load(Ordering::SeqCst), 0);
Ok(())
}
#[test]
fn a_rebind_after_resume_sends_a_freshly_minted_token() -> turmoil::Result {
let script = Script::with([
Serve::Json(200, health_json(true)),
Serve::Json(200, health_json(true)),
]);
let mut sim = sim(0x5EED_0209);
spawn_proxy(&mut sim, Arc::clone(&script));
let minter = Arc::new(SimMinter::new(Arc::clone(&script)));
let auth = Arc::new(ProxyAuth::new(minter, PORT));
let observed = Arc::clone(&auth);
sim.client("harness", async move {
let mut session = session_with_auth(auth);
session.health().await?;
session.rebind(format!("http://{PROXY}:{PORT}"));
session.health().await?;
Ok(())
});
sim.run()?;
assert_eq!(observed.mint_count(), 2);
let seen = script.requests();
assert_eq!(seen[0].header(PROXY_AUTH_HEADER), Some("jwe-scoped-0"));
assert_eq!(
seen[1].header(PROXY_AUTH_HEADER),
Some("jwe-scoped-1"),
"the request after a resume carried the pre-suspend token, whose rejection \
reads exactly like a dead daemon"
);
Ok(())
}
#[test]
fn a_control_plane_wait_reaches_its_deadline_in_simulated_time() -> turmoil::Result {
const TIMEOUT: Duration = Duration::from_secs(30 * 60);
const POLL: Duration = Duration::from_secs(5);
let mut sim = sim_for(0x5EED_0283, Duration::from_secs(60 * 60));
let transport = Arc::new(FakeControlPlane::new());
transport.answer("GetMicrovm", Answer::ok(microvm_response("PENDING", None)));
let observed = Arc::clone(&transport);
sim.client("harness", async move {
let plane = ControlPlane::with_transport(
transport as Arc<dyn Transport>,
Region::US_EAST_1,
Arc::new(SystemClock::new()),
);
let started = tokio::time::Instant::now();
let err = plane
.wait_for_running(
"mvm-abc123",
WaitOpts {
timeout: TIMEOUT,
poll_interval: POLL,
..WaitOpts::for_launch()
},
)
.await
.expect_err("a VM that never leaves PENDING can't be waited into RUNNING");
assert_eq!(err.kind(), ErrorKind::Timeout, "{err}");
assert_eq!(
started.elapsed(),
TIMEOUT,
"the deadline fired at a different simulated instant than the timeout"
);
Ok(())
});
sim.run()?;
let expected = (TIMEOUT.as_secs() / POLL.as_secs()) as usize + 1;
assert_eq!(observed.call_count("GetMicrovm"), expected);
assert_eq!(
observed.calls().len(),
expected,
"the wait asked for an operation it never needs"
);
Ok(())
}