754 lines
24 KiB
Rust
754 lines
24 KiB
Rust
use crate::config::SecretString;
|
|
use crate::control_plane::*;
|
|
use libremetaverse_types::compat::CancellationToken;
|
|
use serde_json::json;
|
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::Duration;
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
use tokio::net::TcpStream;
|
|
|
|
#[derive(Default)]
|
|
struct FakeTarget {
|
|
mutations: Mutex<Vec<String>>,
|
|
shutdown: AtomicBool,
|
|
active: AtomicUsize,
|
|
maximum_active: AtomicUsize,
|
|
}
|
|
|
|
impl FakeTarget {
|
|
fn mutation_count(&self) -> usize {
|
|
self.mutations.lock().expect("mutations").len()
|
|
}
|
|
}
|
|
|
|
impl ControlTarget for FakeTarget {
|
|
#[allow(clippy::too_many_lines)] // Exhaustive fake covers every response family.
|
|
fn execute(
|
|
&self,
|
|
context: ControlContext,
|
|
request: ControlRequest,
|
|
cancellation: CancellationToken,
|
|
) -> ControlFuture<'_> {
|
|
Box::pin(async move {
|
|
let active = self.active.fetch_add(1, Ordering::AcqRel) + 1;
|
|
self.maximum_active.fetch_max(active, Ordering::AcqRel);
|
|
let result = match request {
|
|
ControlRequest::Health => Ok(ControlPayload::Health(HealthView {
|
|
service_state: "running".into(),
|
|
ready: true,
|
|
uptime_seconds: 42,
|
|
protocol_version: CONTROL_PROTOCOL_VERSION,
|
|
})),
|
|
ControlRequest::Runtime => Ok(ControlPayload::Runtime(RuntimeView {
|
|
grid_state: "online".into(),
|
|
generation: 7,
|
|
transport_connected: true,
|
|
agent_ready: true,
|
|
region_id: Some("00000000-0000-4000-8000-000000000001".into()),
|
|
region_handle: Some(9_007_199_254_740_992),
|
|
region_name: Some("Safe Region".into()),
|
|
position: Some([128.0, 128.0, 24.0]),
|
|
behavior_mode: "available".into(),
|
|
control_queue_used: 1,
|
|
control_queue_capacity: 64,
|
|
budget_tool_calls_used: 3,
|
|
budget_movement_millimeters_used: 2_000,
|
|
active_build_transaction: None,
|
|
build_progress: None,
|
|
build_orphan_ids: Vec::new(),
|
|
active_visual_capture: None,
|
|
visual_progress: None,
|
|
visual_image_sha256: None,
|
|
})),
|
|
ControlRequest::ListSessions { page } => Ok(ControlPayload::Sessions(page_values(
|
|
&page,
|
|
(0..7)
|
|
.map(|index| SessionMetadataView {
|
|
session_id: format!("session-{index}"),
|
|
avatar_id: format!("00000000-0000-4000-8000-{index:012x}"),
|
|
channel: "direct_im".into(),
|
|
created_unix_millis: 10,
|
|
last_active_unix_millis: 20,
|
|
turns: 2,
|
|
bytes: 32,
|
|
})
|
|
.collect(),
|
|
))),
|
|
ControlRequest::ListScheduledJobs { page } => {
|
|
Ok(ControlPayload::ScheduledJobs(page_values(
|
|
&page,
|
|
vec![ScheduledJobView {
|
|
job_id: "roam-1".into(),
|
|
kind: "landmark_roam".into(),
|
|
enabled: true,
|
|
next_run_unix_millis: Some(50),
|
|
}],
|
|
)))
|
|
}
|
|
ControlRequest::ListPendingApprovals { page } => {
|
|
Ok(ControlPayload::PendingApprovals(page_values(
|
|
&page,
|
|
vec![PendingApprovalView {
|
|
approval_id: 1,
|
|
tool: "build".into(),
|
|
principal: "operator:local".into(),
|
|
expires_unix_seconds: 99,
|
|
movement_millimeters: 0,
|
|
inventory_operations: 0,
|
|
build_prims: 1,
|
|
}],
|
|
)))
|
|
}
|
|
ControlRequest::ListAuditEvents { page } => {
|
|
Ok(ControlPayload::AuditEvents(page_values(
|
|
&page,
|
|
vec![AuditEventView {
|
|
sequence: 1,
|
|
unix_millis: 1,
|
|
principal: "operator:local".into(),
|
|
operation: "pause_autonomy".into(),
|
|
outcome: "completed".into(),
|
|
authorization_id: None,
|
|
}],
|
|
)))
|
|
}
|
|
ControlRequest::InjectOperatorMessage { message } if message == "wait" => {
|
|
cancellation.cancelled().await;
|
|
Err(ControlError {
|
|
code: ControlErrorCode::Cancelled,
|
|
message: "operation cancelled",
|
|
retryable: false,
|
|
})
|
|
}
|
|
ControlRequest::GracefulShutdown => {
|
|
self.shutdown.store(true, Ordering::Release);
|
|
self.mutations
|
|
.lock()
|
|
.expect("mutations")
|
|
.push("graceful_shutdown".into());
|
|
Ok(ControlPayload::Completed)
|
|
}
|
|
other => {
|
|
assert_eq!(context.role, ControlRole::Operator);
|
|
self.mutations
|
|
.lock()
|
|
.expect("mutations")
|
|
.push(format!("{other:?}"));
|
|
Ok(ControlPayload::Completed)
|
|
}
|
|
};
|
|
self.active.fetch_sub(1, Ordering::AcqRel);
|
|
result
|
|
})
|
|
}
|
|
}
|
|
|
|
fn page_values<T>(page: &PageRequest, values: Vec<T>) -> Page<T> {
|
|
let start = usize::try_from(page.cursor.unwrap_or(0))
|
|
.expect("cursor")
|
|
.min(values.len());
|
|
let end = start
|
|
.saturating_add(usize::from(page.limit))
|
|
.min(values.len());
|
|
let next_cursor = (end < values.len()).then(|| u64::try_from(end).expect("cursor"));
|
|
Page {
|
|
items: values.into_iter().skip(start).take(end - start).collect(),
|
|
next_cursor,
|
|
}
|
|
}
|
|
|
|
fn token(value: &str) -> SecretString {
|
|
SecretString::new("control.test_token", value).expect("token")
|
|
}
|
|
|
|
fn limits() -> ControlLimits {
|
|
ControlLimits {
|
|
request_timeout: Duration::from_secs(5),
|
|
idle_timeout: Duration::from_secs(5),
|
|
write_timeout: Duration::from_secs(1),
|
|
..ControlLimits::default()
|
|
}
|
|
}
|
|
|
|
fn plane(target: Arc<FakeTarget>, configured: ControlLimits) -> Arc<ControlPlane> {
|
|
ControlPlane::new(
|
|
target,
|
|
token("operator-secret"),
|
|
Some(token("observer-secret")),
|
|
configured,
|
|
)
|
|
.expect("plane")
|
|
}
|
|
|
|
fn request(id: &str, request: ControlRequest) -> ControlRequestEnvelope {
|
|
ControlRequestEnvelope {
|
|
version: CONTROL_PROTOCOL_VERSION,
|
|
request_id: id.into(),
|
|
request,
|
|
}
|
|
}
|
|
|
|
enum Client {
|
|
InProcess(InProcessControlClient),
|
|
Tcp(TcpControlClient),
|
|
}
|
|
|
|
impl Client {
|
|
async fn request(&self, envelope: ControlRequestEnvelope) -> ControlResponseEnvelope {
|
|
match self {
|
|
Self::InProcess(client) => client.request(envelope).await,
|
|
Self::Tcp(client) => client.request(envelope).await.expect("TCP response"),
|
|
}
|
|
}
|
|
}
|
|
|
|
enum Transport {
|
|
InProcess,
|
|
Tcp,
|
|
}
|
|
|
|
struct Fixture {
|
|
client: Client,
|
|
plane: Arc<ControlPlane>,
|
|
target: Arc<FakeTarget>,
|
|
server: Option<TcpControlServer>,
|
|
}
|
|
|
|
async fn fixture(transport: Transport, role: ControlRole, configured: ControlLimits) -> Fixture {
|
|
let target = Arc::new(FakeTarget::default());
|
|
let plane = plane(target.clone(), configured);
|
|
let value = if role == ControlRole::Operator {
|
|
"operator-secret"
|
|
} else {
|
|
"observer-secret"
|
|
};
|
|
match transport {
|
|
Transport::InProcess => Fixture {
|
|
client: Client::InProcess(plane.connect(value).expect("connect")),
|
|
plane,
|
|
target,
|
|
server: None,
|
|
},
|
|
Transport::Tcp => {
|
|
let server = TcpControlServer::bind(
|
|
plane.clone(),
|
|
TcpControlConfig {
|
|
listen: "127.0.0.1:0".parse().expect("address"),
|
|
limits: configured,
|
|
},
|
|
)
|
|
.await
|
|
.expect("bind");
|
|
let client = TcpControlClient::connect(server.local_addr(), value, configured)
|
|
.await
|
|
.expect("connect");
|
|
Fixture {
|
|
client: Client::Tcp(client),
|
|
plane,
|
|
target,
|
|
server: Some(server),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn close(fixture: Fixture) {
|
|
drop(fixture.client);
|
|
if let Some(server) = fixture.server {
|
|
server.shutdown().await.expect("shutdown");
|
|
}
|
|
}
|
|
|
|
async fn conformance(transport: Transport) {
|
|
let fixture = fixture(transport, ControlRole::Operator, limits()).await;
|
|
let health = fixture
|
|
.client
|
|
.request(request("health-1", ControlRequest::Health))
|
|
.await;
|
|
assert_eq!(health.version, CONTROL_PROTOCOL_VERSION);
|
|
assert!(matches!(
|
|
health.result,
|
|
Ok(ControlPayload::Health(HealthView { ready: true, .. }))
|
|
));
|
|
let page = fixture
|
|
.client
|
|
.request(request(
|
|
"sessions-1",
|
|
ControlRequest::ListSessions {
|
|
page: PageRequest {
|
|
cursor: Some(2),
|
|
limit: 3,
|
|
},
|
|
},
|
|
))
|
|
.await;
|
|
let Ok(ControlPayload::Sessions(page)) = page.result else {
|
|
panic!("session page")
|
|
};
|
|
assert_eq!(page.items.len(), 3);
|
|
assert_eq!(page.items[0].session_id, "session-2");
|
|
assert_eq!(page.next_cursor, Some(5));
|
|
assert!(
|
|
fixture
|
|
.client
|
|
.request(request("pause-1", ControlRequest::PauseAutonomy))
|
|
.await
|
|
.result
|
|
.is_ok()
|
|
);
|
|
let replay = fixture
|
|
.client
|
|
.request(request("pause-1", ControlRequest::ResumeAutonomy))
|
|
.await;
|
|
assert_eq!(
|
|
replay.result.expect_err("replay").code,
|
|
ControlErrorCode::Replay
|
|
);
|
|
assert_eq!(fixture.target.mutation_count(), 1);
|
|
let shutdown = fixture
|
|
.client
|
|
.request(request("shutdown-1", ControlRequest::GracefulShutdown))
|
|
.await;
|
|
assert!(shutdown.result.is_ok());
|
|
assert!(fixture.target.shutdown.load(Ordering::Acquire));
|
|
close(fixture).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn same_conformance_suite_runs_in_process() {
|
|
conformance(Transport::InProcess).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn same_conformance_suite_runs_over_loopback_tcp() {
|
|
conformance(Transport::Tcp).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn observer_is_read_only_and_tokens_are_redacted() {
|
|
let fixture = fixture(Transport::InProcess, ControlRole::Observer, limits()).await;
|
|
assert!(matches!(
|
|
fixture
|
|
.client
|
|
.request(request("read", ControlRequest::Runtime))
|
|
.await
|
|
.result,
|
|
Ok(ControlPayload::Runtime(_))
|
|
));
|
|
let denied = fixture
|
|
.client
|
|
.request(request("mutate", ControlRequest::PauseAutonomy))
|
|
.await;
|
|
assert_eq!(
|
|
denied.result.expect_err("denied").code,
|
|
ControlErrorCode::PermissionDenied
|
|
);
|
|
assert_eq!(fixture.target.mutation_count(), 0);
|
|
assert!(!format!("{:?}", fixture.plane).contains("operator-secret"));
|
|
let Err(error) = fixture.plane.connect("wrong") else {
|
|
panic!("bad token was accepted")
|
|
};
|
|
assert_eq!(error.code, ControlErrorCode::AuthenticationFailed);
|
|
close(fixture).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn version_bounds_and_malformed_requests_are_typed() {
|
|
let fixture = fixture(Transport::InProcess, ControlRole::Operator, limits()).await;
|
|
let mut wrong = request("version", ControlRequest::Health);
|
|
wrong.version = 99;
|
|
assert_eq!(
|
|
fixture
|
|
.client
|
|
.request(wrong)
|
|
.await
|
|
.result
|
|
.expect_err("version")
|
|
.code,
|
|
ControlErrorCode::VersionMismatch
|
|
);
|
|
let invalid = fixture
|
|
.client
|
|
.request(request(
|
|
"page",
|
|
ControlRequest::ListSessions {
|
|
page: PageRequest {
|
|
cursor: None,
|
|
limit: 101,
|
|
},
|
|
},
|
|
))
|
|
.await;
|
|
assert_eq!(
|
|
invalid.result.expect_err("page").code,
|
|
ControlErrorCode::InvalidRequest
|
|
);
|
|
let secret_payload = fixture
|
|
.client
|
|
.request(request(
|
|
"audit",
|
|
ControlRequest::ListAuditEvents {
|
|
page: PageRequest::default(),
|
|
},
|
|
))
|
|
.await;
|
|
let rendered = serde_json::to_string(&secret_payload).expect("JSON");
|
|
for secret in ["operator-secret", "observer-secret", "Bearer ", "CAPS/"] {
|
|
assert!(!rendered.contains(secret));
|
|
}
|
|
close(fixture).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn cancellation_is_per_request_and_concurrent_operators_stay_responsive() {
|
|
for transport in [Transport::InProcess, Transport::Tcp] {
|
|
let fixture = fixture(transport, ControlRole::Operator, limits()).await;
|
|
let slow = match &fixture.client {
|
|
Client::InProcess(value) => Client::InProcess(value.clone()),
|
|
Client::Tcp(value) => Client::Tcp(value.clone()),
|
|
};
|
|
let task = tokio::spawn(async move {
|
|
slow.request(request(
|
|
"slow",
|
|
ControlRequest::InjectOperatorMessage {
|
|
message: "wait".into(),
|
|
},
|
|
))
|
|
.await
|
|
});
|
|
tokio::time::timeout(Duration::from_secs(1), async {
|
|
while fixture.target.active.load(Ordering::Acquire) == 0 {
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("slow request became active");
|
|
let concurrent_operator = match &fixture.server {
|
|
Some(server) => Client::Tcp(
|
|
TcpControlClient::connect(server.local_addr(), "operator-secret", limits())
|
|
.await
|
|
.expect("second TCP operator"),
|
|
),
|
|
None => Client::InProcess(
|
|
fixture
|
|
.plane
|
|
.connect("operator-secret")
|
|
.expect("second in-process operator"),
|
|
),
|
|
};
|
|
let health = concurrent_operator
|
|
.request(request("health-during", ControlRequest::Health))
|
|
.await;
|
|
assert!(health.result.is_ok());
|
|
let cancelled = fixture
|
|
.client
|
|
.request(request(
|
|
"cancel",
|
|
ControlRequest::CancelRequest {
|
|
target_request_id: "slow".into(),
|
|
},
|
|
))
|
|
.await;
|
|
assert!(matches!(
|
|
cancelled.result,
|
|
Ok(ControlPayload::Cancelled { .. })
|
|
));
|
|
let slow = task.await.expect("slow task");
|
|
assert_eq!(
|
|
slow.result.expect_err("cancelled").code,
|
|
ControlErrorCode::Cancelled
|
|
);
|
|
assert!(fixture.target.maximum_active.load(Ordering::Acquire) >= 2);
|
|
drop(concurrent_operator);
|
|
close(fixture).await;
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn event_stream_resubscribes_with_explicit_gap_and_slow_consumers_are_cut_off() {
|
|
let mut configured = limits();
|
|
configured.event_queue = 2;
|
|
configured.event_history = 3;
|
|
let fixture = fixture(Transport::InProcess, ControlRole::Operator, configured).await;
|
|
let mut in_subscription = None;
|
|
if let Client::InProcess(client) = &fixture.client {
|
|
let (_, subscription) = client
|
|
.subscribe(request(
|
|
"subscribe",
|
|
ControlRequest::SubscribeEvents {
|
|
after_sequence: Some(0),
|
|
},
|
|
))
|
|
.await
|
|
.expect("subscribe");
|
|
in_subscription = Some(subscription);
|
|
}
|
|
for index in 0..10 {
|
|
fixture.plane.publish(ControlEventKind::StateChanged {
|
|
component: "grid".into(),
|
|
state: format!("state-{index}"),
|
|
});
|
|
}
|
|
assert_eq!(fixture.plane.retained_event_count(), 3);
|
|
let first = in_subscription
|
|
.as_mut()
|
|
.expect("subscription")
|
|
.recv()
|
|
.await
|
|
.expect("buffered");
|
|
assert_eq!(first.sequence, 1);
|
|
drop(in_subscription);
|
|
let client = fixture.plane.connect("operator-secret").expect("reconnect");
|
|
let (_, mut resumed) = client
|
|
.subscribe(request(
|
|
"resubscribe",
|
|
ControlRequest::SubscribeEvents {
|
|
after_sequence: Some(1),
|
|
},
|
|
))
|
|
.await
|
|
.expect("resubscribe");
|
|
let gap = resumed.recv().await.expect("gap");
|
|
assert!(matches!(
|
|
gap.event,
|
|
ControlEventKind::Gap {
|
|
first_available: 9,
|
|
last_missed: 8
|
|
}
|
|
));
|
|
close(fixture).await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn zero_client_event_flood_remains_bounded_and_headless() {
|
|
let target = Arc::new(FakeTarget::default());
|
|
let mut configured = limits();
|
|
configured.event_history = 4;
|
|
let plane = plane(target, configured);
|
|
assert_eq!(plane.active_connections(), 0);
|
|
for index in 0..10_000 {
|
|
plane.publish(ControlEventKind::StateChanged {
|
|
component: "load".into(),
|
|
state: index.to_string(),
|
|
});
|
|
}
|
|
assert_eq!(plane.retained_event_count(), 4);
|
|
assert_eq!(plane.active_connections(), 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn subscriptions_have_a_hard_global_bound_and_release_on_drop() {
|
|
let mut configured = limits();
|
|
configured.max_subscriptions = 2;
|
|
let target = Arc::new(FakeTarget::default());
|
|
let plane = plane(target, configured);
|
|
let client = plane.connect("operator-secret").expect("client");
|
|
let (_, first) = client
|
|
.subscribe(request(
|
|
"subscribe-1",
|
|
ControlRequest::SubscribeEvents {
|
|
after_sequence: None,
|
|
},
|
|
))
|
|
.await
|
|
.expect("first subscription");
|
|
let (_, second) = client
|
|
.subscribe(request(
|
|
"subscribe-2",
|
|
ControlRequest::SubscribeEvents {
|
|
after_sequence: None,
|
|
},
|
|
))
|
|
.await
|
|
.expect("second subscription");
|
|
let error = client
|
|
.subscribe(request(
|
|
"subscribe-3",
|
|
ControlRequest::SubscribeEvents {
|
|
after_sequence: None,
|
|
},
|
|
))
|
|
.await
|
|
.err()
|
|
.expect("subscription bound");
|
|
assert_eq!(error.code, ControlErrorCode::Busy);
|
|
drop(first);
|
|
client
|
|
.subscribe(request(
|
|
"subscribe-4",
|
|
ControlRequest::SubscribeEvents {
|
|
after_sequence: None,
|
|
},
|
|
))
|
|
.await
|
|
.expect("released subscription");
|
|
drop(second);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[allow(clippy::too_many_lines)] // One raw-transport matrix shares one listener.
|
|
async fn tcp_rejects_bad_tokens_oversized_and_partial_frames() {
|
|
let configured = limits();
|
|
let target = Arc::new(FakeTarget::default());
|
|
let plane = plane(target, configured);
|
|
let server = TcpControlServer::bind(
|
|
plane.clone(),
|
|
TcpControlConfig {
|
|
listen: "127.0.0.1:0".parse().expect("address"),
|
|
limits: configured,
|
|
},
|
|
)
|
|
.await
|
|
.expect("server");
|
|
let Err(error) = TcpControlClient::connect(server.local_addr(), "wrong", configured).await
|
|
else {
|
|
panic!("bad token was accepted")
|
|
};
|
|
assert_eq!(error.code, ControlErrorCode::AuthenticationFailed);
|
|
|
|
let mut wrong_version = TcpStream::connect(server.local_addr())
|
|
.await
|
|
.expect("version connect");
|
|
let hello = serde_json::to_vec(&json!({
|
|
"frame":"hello",
|
|
"body":{"version":99,"token":"operator-secret"}
|
|
}))
|
|
.expect("hello JSON");
|
|
wrong_version
|
|
.write_all(
|
|
&u32::try_from(hello.len())
|
|
.expect("frame length")
|
|
.to_be_bytes(),
|
|
)
|
|
.await
|
|
.expect("version header");
|
|
wrong_version.write_all(&hello).await.expect("version body");
|
|
let mut response_header = [0_u8; 4];
|
|
wrong_version
|
|
.read_exact(&mut response_header)
|
|
.await
|
|
.expect("version response header");
|
|
let mut response =
|
|
vec![0_u8; usize::try_from(u32::from_be_bytes(response_header)).expect("response length")];
|
|
wrong_version
|
|
.read_exact(&mut response)
|
|
.await
|
|
.expect("version response");
|
|
let response: serde_json::Value = serde_json::from_slice(&response).expect("version JSON");
|
|
assert_eq!(response["body"]["error"]["code"], json!("version_mismatch"));
|
|
|
|
let subscriber = TcpControlClient::connect(server.local_addr(), "operator-secret", configured)
|
|
.await
|
|
.expect("subscriber");
|
|
let subscription_response = subscriber
|
|
.request(request(
|
|
"events",
|
|
ControlRequest::SubscribeEvents {
|
|
after_sequence: None,
|
|
},
|
|
))
|
|
.await
|
|
.expect("subscription response");
|
|
assert!(matches!(
|
|
subscription_response.result,
|
|
Ok(ControlPayload::Subscribed { .. })
|
|
));
|
|
plane.publish(ControlEventKind::StateChanged {
|
|
component: "session".into(),
|
|
state: "online".into(),
|
|
});
|
|
let event = tokio::time::timeout(Duration::from_secs(1), subscriber.next_event())
|
|
.await
|
|
.expect("event deadline")
|
|
.expect("event");
|
|
assert!(matches!(event.event, ControlEventKind::StateChanged { .. }));
|
|
let last_sequence = event.sequence;
|
|
drop(subscriber);
|
|
plane.publish(ControlEventKind::StateChanged {
|
|
component: "session".into(),
|
|
state: "reconnecting".into(),
|
|
});
|
|
let reconnected = TcpControlClient::connect(server.local_addr(), "operator-secret", configured)
|
|
.await
|
|
.expect("reconnected subscriber");
|
|
reconnected
|
|
.request(request(
|
|
"events-resumed",
|
|
ControlRequest::SubscribeEvents {
|
|
after_sequence: Some(last_sequence),
|
|
},
|
|
))
|
|
.await
|
|
.expect("resubscription response");
|
|
let replayed = tokio::time::timeout(Duration::from_secs(1), reconnected.next_event())
|
|
.await
|
|
.expect("replay deadline")
|
|
.expect("replayed event");
|
|
assert_eq!(replayed.sequence, last_sequence.saturating_add(1));
|
|
let mut oversized = TcpStream::connect(server.local_addr())
|
|
.await
|
|
.expect("raw connect");
|
|
oversized
|
|
.write_all(&(u32::try_from(configured.max_frame_bytes).expect("bound") + 1).to_be_bytes())
|
|
.await
|
|
.expect("header");
|
|
let mut byte = [0_u8; 1];
|
|
assert_eq!(
|
|
tokio::time::timeout(Duration::from_secs(1), oversized.read(&mut byte))
|
|
.await
|
|
.expect("closed")
|
|
.expect("read"),
|
|
0
|
|
);
|
|
let mut partial = TcpStream::connect(server.local_addr())
|
|
.await
|
|
.expect("raw connect");
|
|
partial
|
|
.write_all(&100_u32.to_be_bytes())
|
|
.await
|
|
.expect("header");
|
|
partial.write_all(b"{}").await.expect("partial");
|
|
partial.shutdown().await.expect("close");
|
|
drop(reconnected);
|
|
server.shutdown().await.expect("shutdown");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn plaintext_server_rejects_non_loopback_before_binding() {
|
|
let configured = limits();
|
|
let target = Arc::new(FakeTarget::default());
|
|
let plane = plane(target, configured);
|
|
let Err(error) = TcpControlServer::bind(
|
|
plane,
|
|
TcpControlConfig {
|
|
listen: "0.0.0.0:7943".parse().expect("address"),
|
|
limits: configured,
|
|
},
|
|
)
|
|
.await
|
|
else {
|
|
panic!("plaintext remote listener was accepted")
|
|
};
|
|
assert_eq!(error.code, ControlErrorCode::InvalidRequest);
|
|
}
|
|
|
|
#[test]
|
|
fn plain_tcp_is_loopback_only_and_protocol_json_is_stable() {
|
|
let configured = limits();
|
|
assert!(
|
|
!TcpControlConfig {
|
|
listen: "0.0.0.0:7943".parse().expect("address"),
|
|
limits: configured
|
|
}
|
|
.listen
|
|
.ip()
|
|
.is_loopback()
|
|
);
|
|
let encoded = serde_json::to_value(request("health", ControlRequest::Health)).expect("JSON");
|
|
assert_eq!(
|
|
encoded,
|
|
json!({"version":1,"request_id":"health","request":{"method":"health"}})
|
|
);
|
|
}
|