use super::*;
#[test]
fn default_limits_pass_validation_and_carry_signed_numbers()
-> Result<(), Box<dyn std::error::Error>> {
let mut config = sample_config()?;
assert_eq!(config.limits.max_connections, 256);
assert_eq!(config.limits.max_subscriptions_per_connection, 32);
assert_eq!(config.limits.max_conversations_per_connection, 32);
assert_eq!(config.limits.max_pending_pushes_per_connection, 32);
assert_eq!(
config
.limits
.max_pending_conversation_replies_per_connection,
32
);
assert_eq!(config.limits.max_pending_replies_per_conversation, 8);
assert_eq!(config.limits.max_connection_inbox_bytes, 4 * 1024 * 1024);
assert_eq!(config.limits.max_connection_outbound_bytes, 4 * 1024 * 1024);
assert_eq!(
config.limits.max_connection_outbound_bytes, config.limits.max_connection_inbox_bytes,
"the two per-connection byte budgets are deliberately equal"
);
assert_eq!(config.limits.max_subscription_inbox_depth, 4096);
assert_eq!(
config.limits.max_connection_inbox_bytes / config.limits.max_subscription_inbox_depth,
1024,
"the byte-vs-count crossover must stay at 1 KiB per record"
);
assert_eq!(config.limits.delivery_slice_budget, 32);
validate(&mut config, None)?;
Ok(())
}
#[test]
fn zero_limits_are_typed_config_errors() -> Result<(), Box<dyn std::error::Error>> {
type LimitMutator = (&'static str, fn(&mut ServerConfig));
let mutators: [LimitMutator; 10] = [
("max_connections", |c| c.limits.max_connections = 0),
("max_subscriptions_per_connection", |c| {
c.limits.max_subscriptions_per_connection = 0;
}),
("max_conversations_per_connection", |c| {
c.limits.max_conversations_per_connection = 0;
}),
("max_pending_pushes_per_connection", |c| {
c.limits.max_pending_pushes_per_connection = 0;
}),
("max_pending_conversation_replies_per_connection", |c| {
c.limits.max_pending_conversation_replies_per_connection = 0;
}),
("max_pending_replies_per_conversation", |c| {
c.limits.max_pending_replies_per_conversation = 0;
}),
("max_connection_inbox_bytes", |c| {
c.limits.max_connection_inbox_bytes = 0;
}),
("max_connection_outbound_bytes", |c| {
c.limits.max_connection_outbound_bytes = 0;
}),
("max_subscription_inbox_depth", |c| {
c.limits.max_subscription_inbox_depth = 0;
}),
("delivery_slice_budget", |c| {
c.limits.delivery_slice_budget = 0;
}),
];
for (field, mutate) in mutators {
let mut config = sample_config()?;
mutate(&mut config);
let message = config_validation_message(validate(&mut config, None));
assert!(
message.contains(&format!("limits.{field}")),
"zero {field} must report a typed limits.{field} error, got: {message}"
);
assert!(
message.contains("greater than zero"),
"the {field} refusal must say why: {message}"
);
}
Ok(())
}
const fn sample_participant() -> ParticipantConfig {
ParticipantConfig {
wire_frame_limit: 65_536,
attach_receipt_ttl_ms: 60_000,
receipt_provenance_ttl_ms: 600_000,
live_receipt_server_report_threshold: 1_024,
max_live_attach_receipts_per_participant: 8,
receipt_provenance_server_report_threshold: 4_096,
receipt_provenance_per_conversation_report_threshold: 256,
max_receipt_provenance_per_participant: 64,
max_retired_identity_slots_server: 1_024,
identity_slots: 4,
observer_recovery_max_entries: 64,
max_semantic_conversations_per_connection: 32,
max_ordinary_record_entries: 1,
max_ordinary_record_bytes: 131_072,
max_generated_marker_entries: 1,
max_generated_marker_bytes: 4_096,
mandatory_transaction_bound_entries: 4,
mandatory_transaction_bound_bytes: 16_384,
full_recovery_claim_entries: 4,
full_recovery_claim_bytes: 16_384,
retained_capacity_entries: 2_048,
retained_capacity_bytes: 16_777_216,
max_retained_record_rows: 1_024,
pruning: None,
closure_episode_churn_limit: 1_024,
}
}
#[test]
fn valid_participant_section_passes_validation() -> Result<(), Box<dyn std::error::Error>> {
let mut config = sample_config()?;
config.participant = Some(sample_participant());
let temp_dir = std::env::temp_dir().join(format!(
"liminal-server-participant-validation-{}-{}",
std::process::id(),
NEXT_TEMP_DIR_ID.fetch_add(1, Ordering::Relaxed)
));
fs::create_dir_all(&temp_dir)?;
config.persistence_path = Some(temp_dir.clone());
let result = validate(&mut config, None);
fs::remove_dir_all(&temp_dir)?;
assert!(result.is_ok(), "expected valid config, got {result:?}");
Ok(())
}
#[test]
fn zero_participant_values_are_typed_config_errors() -> Result<(), Box<dyn std::error::Error>> {
type ParticipantMutator = (&'static str, fn(&mut ParticipantConfig));
let mutators: [ParticipantMutator; 12] = [
("wire_frame_limit", |p| p.wire_frame_limit = 0),
("attach_receipt_ttl_ms", |p| p.attach_receipt_ttl_ms = 0),
("receipt_provenance_ttl_ms", |p| {
p.receipt_provenance_ttl_ms = 0;
}),
("live_receipt_server_report_threshold", |p| {
p.live_receipt_server_report_threshold = 0;
}),
("max_live_attach_receipts_per_participant", |p| {
p.max_live_attach_receipts_per_participant = 0;
}),
("receipt_provenance_server_report_threshold", |p| {
p.receipt_provenance_server_report_threshold = 0;
}),
(
"receipt_provenance_per_conversation_report_threshold",
|p| {
p.receipt_provenance_per_conversation_report_threshold = 0;
},
),
("max_receipt_provenance_per_participant", |p| {
p.max_receipt_provenance_per_participant = 0;
}),
("max_retired_identity_slots_server", |p| {
p.max_retired_identity_slots_server = 0;
}),
("identity_slots", |p| p.identity_slots = 0),
("observer_recovery_max_entries", |p| {
p.observer_recovery_max_entries = 0;
}),
("max_semantic_conversations_per_connection", |p| {
p.max_semantic_conversations_per_connection = 0;
}),
];
for (field, mutate) in mutators {
let mut config = sample_config()?;
let mut participant = sample_participant();
mutate(&mut participant);
config.participant = Some(participant);
let message = config_validation_message(validate(&mut config, None));
assert!(
message.contains(&format!("participant.{field}")),
"expected typed error for participant.{field}, got: {message}"
);
}
Ok(())
}
#[test]
fn provenance_ttl_shorter_than_receipt_is_a_typed_config_error()
-> Result<(), Box<dyn std::error::Error>> {
let mut config = sample_config()?;
let mut participant = sample_participant();
participant.receipt_provenance_ttl_ms = participant.attach_receipt_ttl_ms - 1;
config.participant = Some(participant);
let message = config_validation_message(validate(&mut config, None));
assert!(
message.contains("participant.receipt_provenance_ttl_ms"),
"expected TTL-ordering error, got: {message}"
);
Ok(())
}
#[test]
fn undersized_wire_frame_limit_is_a_typed_config_error() -> Result<(), Box<dyn std::error::Error>> {
let mut config = sample_config()?;
let mut participant = sample_participant();
participant.wire_frame_limit = 1;
config.participant = Some(participant);
let message = config_validation_message(validate(&mut config, None));
assert!(
message.contains("participant.wire_frame_limit"),
"expected minimum-frame error, got: {message}"
);
Ok(())
}
#[test]
fn worker_front_door_rejects_participant_section() -> Result<(), Box<dyn std::error::Error>> {
let mut config = worker_front_door_config()?;
config.participant = Some(sample_participant());
let message = config_validation_message(validate(&mut config, None));
assert!(
message.contains("installs no participant service"),
"expected worker-front-door participant rejection, got: {message}"
);
Ok(())
}