315 lines
10 KiB
Rust
315 lines
10 KiB
Rust
//! Tests for `BrokerPort` against a fake `brokerd` on a temporary socket: answers, approvals and
|
|
//! timeouts. Frames that break the protocol are in `broker_port_bad.rs`. Do not edit.
|
|
|
|
#[path = "support/broker.rs"]
|
|
mod fake;
|
|
|
|
use std::io::Write;
|
|
use std::os::unix::net::UnixListener;
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
|
|
use fake::{assert_unavailable, broker, call, frame, in_ms, request, result, send, socket_path};
|
|
use loopd::broker_port::{BrokerPort, NoBroker, POINTER, TOO_LARGE};
|
|
use loopd::tools::{Pending, ToolPort};
|
|
use proto::{
|
|
DenyReason, Message, PROTOCOL_VERSION, Timestamp, ToolResponse, read_frame, write_frame,
|
|
};
|
|
|
|
#[test]
|
|
fn a_result_comes_back_as_it_was_sent() {
|
|
let (socket, broker) = broker(|stream, request| {
|
|
send(stream, request.id, true, result("hello\n"));
|
|
});
|
|
let got = call(socket, 2_000, &request());
|
|
assert_eq!(got.response, result("hello\n"));
|
|
assert!(got.pending.is_empty());
|
|
assert!(got.lines.is_empty(), "nothing to report: {:?}", got.lines);
|
|
|
|
// What the broker received: one final frame holding exactly the request.
|
|
let sent = broker.join().unwrap();
|
|
assert_eq!(sent.v, PROTOCOL_VERSION);
|
|
assert!(sent.r#final, "a request is a single final frame");
|
|
assert_eq!(sent.msg, Message::ToolRequest(request()));
|
|
}
|
|
|
|
#[test]
|
|
fn every_denial_and_a_failure_come_back_as_they_were_sent() {
|
|
let mut answers: Vec<ToolResponse> = [
|
|
DenyReason::NoGrant,
|
|
DenyReason::GrantExpired,
|
|
DenyReason::TaintTooHigh,
|
|
DenyReason::DeniedByGrant,
|
|
DenyReason::ApprovalRefused,
|
|
DenyReason::ApprovalExpired,
|
|
DenyReason::GrantsInvalid,
|
|
DenyReason::AuditUnavailable,
|
|
DenyReason::InvalidArguments,
|
|
DenyReason::StateUnreadable,
|
|
]
|
|
.into_iter()
|
|
.map(|reason| ToolResponse::Denied { reason })
|
|
.collect();
|
|
answers.push(ToolResponse::Failed {
|
|
message: "the runner arrives in M3b".to_string(),
|
|
});
|
|
for answer in answers {
|
|
let reply = answer.clone();
|
|
let (socket, broker) = broker(move |stream, request| {
|
|
send(stream, request.id, true, reply);
|
|
});
|
|
let got = call(socket, 2_000, &request());
|
|
assert_eq!(got.response, answer);
|
|
assert!(
|
|
got.lines.is_empty(),
|
|
"a denial is not an outage: {:?}",
|
|
got.lines
|
|
);
|
|
broker.join().unwrap();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_pending_frame_reaches_the_callback_and_the_final_frame_is_the_answer() {
|
|
let expires = in_ms(60_000);
|
|
let (socket, broker) = broker(move |stream, request| {
|
|
let pending = ToolResponse::PendingApproval {
|
|
approval: 41,
|
|
expires,
|
|
};
|
|
send(stream, request.id, false, pending);
|
|
thread::sleep(Duration::from_millis(150));
|
|
send(stream, request.id, true, result("approved"));
|
|
});
|
|
let got = call(socket, 2_000, &request());
|
|
assert_eq!(got.response, result("approved"));
|
|
assert_eq!(
|
|
got.pending,
|
|
[Pending {
|
|
approval: 41,
|
|
expires
|
|
}],
|
|
"called once, with the frame's values"
|
|
);
|
|
assert!(got.lines.is_empty(), "{:?}", got.lines);
|
|
broker.join().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn an_answer_after_expires_but_within_the_timeout_of_it_is_delivered() {
|
|
// expires in 100 ms, timeout 1,500 ms: the port waits until 1,600 ms. The answer comes at
|
|
// about 400 ms, well after `expires`: an approval given at the last moment still gets the
|
|
// whole timeout to run.
|
|
let (socket, broker) = broker(|stream, request| {
|
|
let pending = ToolResponse::PendingApproval {
|
|
approval: 1,
|
|
expires: in_ms(100),
|
|
};
|
|
send(stream, request.id, false, pending);
|
|
thread::sleep(Duration::from_millis(400));
|
|
send(stream, request.id, true, result("late but good"));
|
|
});
|
|
let got = call(socket, 1_500, &request());
|
|
assert_eq!(got.response, result("late but good"));
|
|
assert!(got.lines.is_empty(), "{:?}", got.lines);
|
|
broker.join().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn an_expiry_that_has_already_passed_still_leaves_the_timeout() {
|
|
let (socket, broker) = broker(|stream, request| {
|
|
let pending = ToolResponse::PendingApproval {
|
|
approval: 1,
|
|
expires: Timestamp::parse("2020-01-01T00:00:00.000Z").unwrap(),
|
|
};
|
|
send(stream, request.id, false, pending);
|
|
thread::sleep(Duration::from_millis(200));
|
|
send(stream, request.id, true, result("fine"));
|
|
});
|
|
let got = call(socket, 1_500, &request());
|
|
assert_eq!(got.response, result("fine"));
|
|
broker.join().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn no_socket_is_unavailable() {
|
|
let socket = socket_path();
|
|
let _ = std::fs::remove_file(&socket);
|
|
let got = call(socket.clone(), 2_000, &request());
|
|
assert_unavailable(&got, "no socket file");
|
|
assert!(
|
|
got.lines[0].contains(&socket.display().to_string()),
|
|
"the line names the socket: {}",
|
|
got.lines[0]
|
|
);
|
|
assert!(got.took < Duration::from_millis(1_500), "{:?}", got.took);
|
|
}
|
|
|
|
#[test]
|
|
fn a_broker_that_closes_without_answering_is_unavailable() {
|
|
let (socket, broker) = broker(|_, _| {});
|
|
let got = call(socket, 2_000, &request());
|
|
assert_unavailable(&got, "closed before any frame");
|
|
assert!(
|
|
got.took < Duration::from_millis(1_500),
|
|
"a close is seen at once: {:?}",
|
|
got.took
|
|
);
|
|
broker.join().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn a_broker_that_closes_while_pending_is_unavailable() {
|
|
let (socket, broker) = broker(|stream, request| {
|
|
let pending = ToolResponse::PendingApproval {
|
|
approval: 3,
|
|
expires: in_ms(60_000),
|
|
};
|
|
send(stream, request.id, false, pending);
|
|
// brokerd was restarted: the connection just ends.
|
|
});
|
|
let got = call(socket, 2_000, &request());
|
|
assert_unavailable(&got, "closed while pending");
|
|
assert_eq!(got.pending.len(), 1, "the pending frame was reported first");
|
|
assert!(got.took < Duration::from_millis(1_500), "{:?}", got.took);
|
|
broker.join().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn a_broker_that_never_answers_is_unavailable_after_the_timeout() {
|
|
let (socket, broker) = broker(|_, _| thread::sleep(Duration::from_millis(1_200)));
|
|
let got = call(socket, 300, &request());
|
|
assert_unavailable(&got, "silence");
|
|
assert!(
|
|
got.took >= Duration::from_millis(250),
|
|
"gave up early: {:?}",
|
|
got.took
|
|
);
|
|
assert!(
|
|
got.took < Duration::from_millis(1_100),
|
|
"gave up late: {:?}",
|
|
got.took
|
|
);
|
|
broker.join().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn a_pending_call_nobody_answers_ends_at_expires_plus_the_timeout() {
|
|
let (socket, broker) = broker(|stream, request| {
|
|
let pending = ToolResponse::PendingApproval {
|
|
approval: 3,
|
|
expires: in_ms(300),
|
|
};
|
|
send(stream, request.id, false, pending);
|
|
thread::sleep(Duration::from_millis(1_800));
|
|
});
|
|
let got = call(socket, 300, &request());
|
|
assert_unavailable(&got, "silence while pending");
|
|
assert!(
|
|
got.took >= Duration::from_millis(550),
|
|
"it must wait for expires (300) plus the timeout (300): {:?}",
|
|
got.took
|
|
);
|
|
assert!(
|
|
got.took < Duration::from_millis(1_700),
|
|
"gave up late: {:?}",
|
|
got.took
|
|
);
|
|
broker.join().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn the_timeout_is_a_deadline_for_the_frame_not_for_each_read() {
|
|
// The four header bytes arrive 250 ms apart, the last at 1,000 ms. A port that only sets a
|
|
// 600 ms read timeout sees every single read succeed and returns the result; a port with a
|
|
// deadline gives up at 600 ms. The answer tells them apart, so no timing is asserted.
|
|
let (socket, broker) = broker(|stream, request| {
|
|
let mut bytes = Vec::new();
|
|
write_frame(
|
|
&mut bytes,
|
|
&frame(request.id, true, Message::ToolResponse(result("slow"))),
|
|
)
|
|
.unwrap();
|
|
for byte in bytes.iter().take(4) {
|
|
thread::sleep(Duration::from_millis(250));
|
|
if stream.write_all(&[*byte]).is_err() {
|
|
return;
|
|
}
|
|
}
|
|
let _ = stream.write_all(&bytes[4..]);
|
|
});
|
|
let got = call(socket, 600, &request());
|
|
assert_unavailable(&got, "a trickled frame");
|
|
broker.join().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn a_zero_timeout_fails_closed_and_does_not_panic() {
|
|
let (socket, _broker) = broker(|stream, request| {
|
|
send(stream, request.id, true, result("too late"));
|
|
});
|
|
let got = call(socket, 0, &request());
|
|
assert_unavailable(&got, "timeout_ms = 0");
|
|
}
|
|
|
|
#[test]
|
|
fn a_request_too_large_for_a_frame_is_its_own_failure() {
|
|
// Nothing is sent, so the fake broker sees a connection that closes or none at all.
|
|
let path = socket_path();
|
|
let _ = std::fs::remove_file(&path);
|
|
let _listener = UnixListener::bind(&path).unwrap();
|
|
let mut big = request();
|
|
big.arguments = format!(r#"{{"path":"/{}"}}"#, "a".repeat(proto::MAX_FRAME));
|
|
let got = call(path, 1_000, &big);
|
|
assert_eq!(
|
|
got.response,
|
|
ToolResponse::Failed {
|
|
message: TOO_LARGE.to_string()
|
|
}
|
|
);
|
|
assert!(
|
|
got.lines.is_empty(),
|
|
"the broker is fine; this is not an outage: {:?}",
|
|
got.lines
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn every_call_is_its_own_connection() {
|
|
let path = socket_path();
|
|
let _ = std::fs::remove_file(&path);
|
|
let listener = UnixListener::bind(&path).unwrap();
|
|
let server = thread::spawn(move || {
|
|
for n in 0..3u64 {
|
|
let (mut stream, _) = listener.accept().unwrap();
|
|
let request = read_frame(&mut stream).unwrap();
|
|
send(
|
|
&mut stream,
|
|
request.id,
|
|
true,
|
|
result(&format!("answer {n}")),
|
|
);
|
|
}
|
|
});
|
|
let port = BrokerPort::new(path, Duration::from_millis(2_000));
|
|
for n in 0..3 {
|
|
let got = port.call(&request(), &mut |_| {});
|
|
assert_eq!(got, result(&format!("answer {n}")));
|
|
}
|
|
server.join().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn without_a_broker_every_call_fails_in_words_the_model_can_repeat() {
|
|
let mut seen = 0;
|
|
let got = NoBroker.call(&request(), &mut |_| seen += 1);
|
|
assert_eq!(
|
|
got,
|
|
ToolResponse::Failed {
|
|
message: "no tool broker is configured".to_string()
|
|
}
|
|
);
|
|
assert_eq!(seen, 0);
|
|
assert_eq!(POINTER, "see docs/runbook.md#broker-unavailable");
|
|
}
|