use std::time::{Duration, Instant};
use serde_json::json;
use super::*;
fn connection_config_for_state_tests(
protocol: OcppVersion,
) -> SimulatorConnectionConfig {
SimulatorConnectionConfig {
profile: None,
ws_url: "ws://localhost:9000/ocpp".to_string(),
cp_id: "CP-TEST".to_string(),
append_cp_id: false,
connectors: 2,
protocol,
vendor: "ocppsim".to_string(),
model: "test".to_string(),
firmware: "0.0.0".to_string(),
trace_frames: false,
strict: false,
request_timeout: Duration::from_secs(30),
heartbeat_seconds: Some(10),
outbound_queue_limit: 1_000,
security_event_limit: 1_000,
security_profile: None,
basic_auth_password: None,
ca_cert_path: None,
client_cert_path: None,
client_key_path: None,
}
}
#[tokio::test]
async fn connect_without_target_warns_and_stays_offline() {
let (mut simulator, mut ui_rx) =
simulator_for_tests_with_protocol_and_ui(OcppVersion::V1_6);
simulator.config.ws_url = None;
simulator.config.cp_id = None;
let outcome = simulator
.handle_offline_command(SimulatorCommand::Connect { config: None })
.await
.expect("offline connect should be handled");
assert!(matches!(outcome, super::super::OfflineOutcome::Continue));
assert!(!simulator.connected);
let mut events = Vec::new();
while let Ok(event) = ui_rx.try_recv() {
events.push(event);
}
assert!(events.iter().any(|event| {
matches!(
event,
UiEvent::Log {
level: UiLogLevel::Warn,
message,
} if message.contains("No connection target configured")
)
}));
}
#[test]
fn emit_snapshot_redacts_connection_url_secrets() {
let (mut simulator, mut ui_rx) =
simulator_for_tests_with_protocol_and_ui(OcppVersion::V1_6);
simulator.config.ws_url =
Some("wss://user:secret@example.test/ocpp?token=SECRET".to_string());
simulator.emit_snapshot();
let snapshot = loop {
match ui_rx.try_recv().expect("snapshot event should be emitted") {
UiEvent::Snapshot(snapshot) => break snapshot,
UiEvent::RuntimeState(_) | UiEvent::Log { .. } => {}
}
};
assert_eq!(
snapshot.connection_url,
"wss://<redacted>@example.test/ocpp?token=<redacted>"
);
}
#[test]
fn get_diagnostics_redacts_location_url_secrets_in_logs() {
let (mut simulator, mut ui_rx) =
simulator_for_tests_with_protocol_and_ui(OcppVersion::V1_6);
let payload = json!({
"location": "https://user:secret@example.test/logs?token=SECRET"
});
let _ = simulator
.get_diagnostics_v1_6(&payload)
.expect("diagnostics response should be built");
let messages = drain_log_messages(&mut ui_rx);
assert!(
messages
.iter()
.all(|message| !message.contains("secret") && !message.contains("SECRET"))
);
assert!(messages.iter().any(|message| {
message.contains("https://<redacted>@example.test/logs?token=<redacted>")
}));
}
#[test]
fn trace_frame_redacts_url_secrets_in_string_values() {
let frame = json!([
2,
"m1",
"GetDiagnostics",
{ "location": "https://user:secret@example.test/logs?token=SECRET" }
])
.to_string();
let trace = sanitized_trace_frame_text(&frame);
assert!(!trace.contains("secret"));
assert!(!trace.contains("SECRET"));
assert!(
trace.contains("https://<redacted>@example.test/logs?token=<redacted>")
);
}
#[test]
fn trace_frame_redacts_token_aliases() {
let frame = json!([
3,
"m1",
{
"idTagInfo": {
"parentIdTag": "PARENT-TOKEN",
"status": "Accepted"
},
"idToken": {
"additionalInfo": [
{
"additionalIdToken": "ADDITIONAL-TOKEN",
"type": "PaymentBrand"
}
],
"idToken": "PRIMARY-TOKEN",
"type": "Central"
}
}
])
.to_string();
let trace = sanitized_trace_frame_text(&frame);
for token in ["PARENT-TOKEN", "ADDITIONAL-TOKEN", "PRIMARY-TOKEN"] {
assert!(!trace.contains(token), "{token} appeared in {trace}");
}
assert!(trace.contains("\"parentIdTag\":\"<redacted>\""));
assert!(trace.contains("\"additionalIdToken\":\"<redacted>\""));
assert!(trace.contains("\"idToken\":\"<redacted>\""));
}
#[test]
fn outbound_queue_limit_drops_new_calls() {
let (mut simulator, mut ui_rx) =
simulator_for_tests_with_protocol_and_ui(OcppVersion::V1_6);
simulator.config.outbound_queue_limit = 1;
simulator.enqueue_heartbeat();
simulator
.enqueue_authorize("TOKEN".to_string())
.expect("authorize should validate");
assert_eq!(simulator.queue.len(), 1);
assert_eq!(
simulator.queue.front().map(|call| call.action.as_str()),
Some("Heartbeat")
);
let messages = drain_log_messages(&mut ui_rx);
assert!(messages.iter().any(|message| {
message.contains("Outbound OCPP queue limit 1 reached")
&& message.contains("Authorize")
}));
}
#[test]
fn boot_state_is_unchanged_when_queue_is_full() {
let mut simulator = simulator_for_tests();
simulator.config.outbound_queue_limit = 1;
simulator.boot_registration_status = BootRegistrationStatus::Accepted;
simulator.enqueue_heartbeat();
let error = simulator
.enqueue_boot_with_registration_state()
.expect_err("boot should not enqueue into a full queue");
assert!(error.to_string().contains("BootNotification"));
assert_eq!(
simulator.boot_registration_status,
BootRegistrationStatus::Accepted
);
assert_eq!(
simulator.queue.front().map(|call| call.action.as_str()),
Some("Heartbeat")
);
}
#[test]
fn reconnect_boot_depends_on_registration_and_payload_changes() {
let mut simulator = simulator_for_tests();
assert!(
simulator
.should_enqueue_boot_on_connect()
.expect("boot check")
);
simulator
.enqueue_boot_notification()
.expect("boot should validate");
simulator.queue.clear();
simulator.boot_registration_status = BootRegistrationStatus::Accepted;
assert!(
!simulator
.should_enqueue_boot_on_connect()
.expect("boot check")
);
simulator.boot_registration_status = BootRegistrationStatus::Pending;
assert!(
simulator
.should_enqueue_boot_on_connect()
.expect("boot check")
);
simulator.boot_registration_status = BootRegistrationStatus::Accepted;
simulator.config.firmware = "1.2.3".to_string();
assert!(
simulator
.should_enqueue_boot_on_connect()
.expect("boot check")
);
}
#[test]
fn connection_scope_change_resets_boot_registration_state() {
let mut simulator = simulator_for_tests();
simulator
.enqueue_boot_notification()
.expect("boot should validate");
simulator.queue.clear();
simulator.boot_registration_status = BootRegistrationStatus::Accepted;
assert!(
!simulator
.should_enqueue_boot_on_connect()
.expect("boot check")
);
simulator.apply_connection_config(connection_config_for_state_tests(
OcppVersion::V1_6,
));
assert_eq!(
simulator.boot_registration_status,
BootRegistrationStatus::Accepted
);
assert!(
!simulator
.should_enqueue_boot_on_connect()
.expect("boot check")
);
let mut next_config = connection_config_for_state_tests(OcppVersion::V1_6);
next_config.cp_id = "CP-OTHER".to_string();
simulator.apply_connection_config(next_config);
assert_eq!(
simulator.boot_registration_status,
BootRegistrationStatus::Rejected
);
assert!(simulator.last_boot_notification_payload.is_none());
assert!(
simulator
.should_enqueue_boot_on_connect()
.expect("boot check")
);
}
#[test]
fn protocol_security_scope_changes_reset_boot_registration_state() {
let mut simulator = simulator_for_tests();
simulator
.enqueue_boot_notification()
.expect("boot should validate");
simulator.queue.clear();
simulator.boot_registration_status = BootRegistrationStatus::Accepted;
let mut next_config = connection_config_for_state_tests(OcppVersion::V2_0_1);
next_config.security_profile = Some(2);
next_config.basic_auth_password = Some("abcdefghijklmnop".to_string());
simulator.apply_connection_config(next_config);
assert_eq!(
simulator.boot_registration_status,
BootRegistrationStatus::Rejected
);
assert!(simulator.last_boot_notification_payload.is_none());
assert!(
simulator
.should_enqueue_boot_on_connect()
.expect("boot check")
);
}
#[test]
fn trace_frame_redacts_network_profile_secrets() {
let frame = json!([
2,
"m1",
"SetNetworkProfile",
{
"configurationSlot": 1,
"connectionData": {
"apn": {
"apn": "internet",
"apnAuthentication": "CHAP",
"apnPassword": "APN-PASSWORD",
"simPin": 9876
},
"basicAuthPassword": "BASIC-PASSWORD",
"messageTimeout": 30,
"ocppCsmsUrl": "wss://user:secret@example.test/ocpp?token=SECRET",
"ocppInterface": "Wired0",
"ocppTransport": "JSON",
"ocppVersion": "OCPP20",
"securityProfile": 2,
"vpn": {
"group": "group",
"key": "VPN-KEY",
"password": "VPN-PASSWORD",
"server": "vpn.example.test",
"type": "IKEv2",
"user": "user"
}
}
}
])
.to_string();
let trace = sanitized_trace_frame_text(&frame);
for secret in [
"APN-PASSWORD",
"BASIC-PASSWORD",
"VPN-KEY",
"VPN-PASSWORD",
"9876",
"secret",
"SECRET",
] {
assert!(!trace.contains(secret), "{secret} appeared in {trace}");
}
assert!(trace.contains("\"apnPassword\":\"<redacted>\""));
assert!(trace.contains("\"basicAuthPassword\":\"<redacted>\""));
assert!(trace.contains("\"key\":\"<redacted>\""));
assert!(trace.contains("\"password\":\"<redacted>\""));
assert!(trace.contains("\"simPin\":\"<redacted>\""));
assert!(
trace.contains("wss://<redacted>@example.test/ocpp?token=<redacted>")
);
}
#[test]
fn reserve_and_cancel_updates_connector_status() {
let mut simulator = simulator_for_tests();
let reserve_payload = json!({
"connectorId": 1,
"expiryDate": now_timestamp(),
"idTag": "TOKEN",
"reservationId": 42
});
let cancel_payload = json!({
"reservationId": 42
});
let reserve_status = simulator
.reserve_now_v1_6(&reserve_payload)
.expect("reserve should succeed");
assert_eq!(reserve_status, ResponseStatus::Accepted);
assert_eq!(
simulator
.connectors
.get(&1)
.map(|item| item.status.display()),
Some("Reserved")
);
let cancel_status = simulator
.cancel_reservation_v1_6(&cancel_payload)
.expect("cancel should succeed");
assert_eq!(cancel_status, ResponseStatus::Accepted);
assert_eq!(
simulator
.connectors
.get(&1)
.map(|item| item.status.display()),
Some("Available")
);
}
#[test]
fn cancel_reservation_preserves_inoperative_connector_status() {
let mut simulator = simulator_for_tests();
let reserve_payload = json!({
"connectorId": 1,
"expiryDate": now_timestamp(),
"idTag": "TOKEN",
"reservationId": 42
});
let inoperative_payload = json!({
"connectorId": 1,
"type": "Inoperative"
});
let cancel_payload = json!({
"reservationId": 42
});
assert_eq!(
simulator
.reserve_now_v1_6(&reserve_payload)
.expect("reserve should succeed"),
ResponseStatus::Accepted
);
assert_eq!(
simulator
.change_availability_v1_6(&inoperative_payload)
.expect("inoperative should succeed"),
ResponseStatus::Accepted
);
assert_eq!(
simulator
.cancel_reservation_v1_6(&cancel_payload)
.expect("cancel should succeed"),
ResponseStatus::Accepted
);
assert_eq!(
simulator
.connectors
.get(&1)
.map(|item| item.status.display()),
Some("Unavailable")
);
}
#[test]
fn duplicate_reservation_id_is_rejected() {
let mut simulator = simulator_for_tests();
let first = json!({
"connectorId": 1,
"expiryDate": now_timestamp(),
"idTag": "TOKEN",
"reservationId": 42
});
let duplicate = json!({
"connectorId": 2,
"expiryDate": now_timestamp(),
"idTag": "TOKEN",
"reservationId": 42
});
assert_eq!(
simulator.reserve_now_v1_6(&first).expect("reserve first"),
ResponseStatus::Accepted
);
assert_eq!(
simulator
.reserve_now_v1_6(&duplicate)
.expect("reserve duplicate"),
ResponseStatus::Rejected
);
assert_eq!(
simulator
.connectors
.get(&1)
.map(|item| item.status.display()),
Some("Reserved")
);
assert_eq!(
simulator
.connectors
.get(&2)
.map(|item| item.status.display()),
Some("Available")
);
}
#[test]
fn reserve_now_reports_faulted_connector_status() {
let mut simulator = simulator_for_tests();
simulator.connector_mut(1).expect("connector").status =
ConnectorStatus::Faulted;
assert_eq!(
simulator
.reserve_now_v1_6(&json!({
"connectorId": 1,
"expiryDate": now_timestamp(),
"idTag": "TOKEN",
"reservationId": 42
}))
.expect("reserve faulted connector"),
ResponseStatus::Faulted
);
assert!(simulator.queue.is_empty());
for_each_v2_x_simulator(|_, mut simulator| {
simulator.connector_mut(1).expect("connector").status =
ConnectorStatus::Faulted;
assert_eq!(
simulator
.reserve_now_v2_x(&json!({
"evseId": 1,
"expiryDateTime": now_timestamp(),
"id": 42,
"idToken": {
"idToken": "TOKEN",
"type": "Central"
}
}))
.expect("reserve faulted evse"),
ResponseStatus::Faulted
);
assert!(simulator.queue.is_empty());
});
}
#[test]
fn scheduled_availability_applies_after_stop() {
let mut simulator = simulator_for_tests();
simulator
.start_transaction(1, "TOKEN".to_string(), false, None, false)
.expect("start should succeed");
let status = simulator
.change_availability_v1_6(&json!({
"connectorId": 1,
"type": "Inoperative"
}))
.expect("change availability");
assert_eq!(status, ResponseStatus::Scheduled);
simulator
.stop_transaction(1, Some("Local"), false, false)
.expect("stop should succeed");
assert_eq!(
simulator
.connectors
.get(&1)
.map(|item| item.status.display()),
Some("Unavailable")
);
}
#[test]
fn operative_availability_preserves_active_transaction_status() {
let mut simulator = simulator_for_tests();
simulator
.start_transaction(1, "TOKEN".to_string(), false, None, false)
.expect("start should succeed");
simulator.queue.clear();
let status = simulator
.change_availability_v1_6(&json!({
"connectorId": 1,
"type": "Operative"
}))
.expect("change availability");
assert_eq!(status, ResponseStatus::Accepted);
assert_eq!(
simulator
.connectors
.get(&1)
.map(|item| item.status.display()),
Some("Charging")
);
assert!(simulator.queue.is_empty());
}
#[test]
fn operative_availability_cancels_scheduled_inoperative_change() {
let mut simulator = simulator_for_tests();
simulator
.start_transaction(1, "TOKEN".to_string(), false, None, false)
.expect("start should succeed");
assert_eq!(
simulator
.change_availability_v1_6(&json!({
"connectorId": 1,
"type": "Inoperative"
}))
.expect("schedule availability"),
ResponseStatus::Scheduled
);
assert_eq!(
simulator
.change_availability_v1_6(&json!({
"connectorId": 1,
"type": "Operative"
}))
.expect("cancel scheduled availability"),
ResponseStatus::Accepted
);
simulator
.stop_transaction(1, Some("Local"), false, false)
.expect("stop should succeed");
assert_eq!(
simulator
.connectors
.get(&1)
.map(|item| item.status.display()),
Some("Finishing")
);
}
#[test]
fn change_availability_v1_6_connector_zero_updates_all_connectors() {
let mut simulator = simulator_for_tests();
let status = simulator
.change_availability_v1_6(&json!({
"connectorId": 0,
"type": "Inoperative"
}))
.expect("change availability");
assert_eq!(status, ResponseStatus::Accepted);
assert!(
simulator
.connectors
.values()
.all(|connector| { connector.status == ConnectorStatus::Unavailable })
);
assert_eq!(
simulator
.queue
.iter()
.filter(|call| call.action == "StatusNotification")
.count(),
2
);
}
#[test]
fn change_availability_v1_6_requires_connector_id() {
let mut simulator = simulator_for_tests();
let result = simulator.change_availability_v1_6(&json!({
"type": "Inoperative"
}));
assert!(result.is_err());
assert!(simulator.queue.is_empty());
assert!(
simulator
.connectors
.values()
.all(|connector| { connector.status == ConnectorStatus::Available })
);
}
#[test]
fn accepted_boot_result_enqueues_v1_6_status_for_charge_point_and_connectors() {
let mut simulator = simulator_for_tests();
simulator.boot_retry_after = Some(Instant::now() + Duration::from_secs(10));
simulator
.apply_call_result_context(
&PendingContext::Boot,
&json!({
"status": "Accepted",
"currentTime": now_timestamp()
}),
)
.expect("boot result");
assert_eq!(
simulator.boot_registration_status,
BootRegistrationStatus::Accepted
);
assert!(simulator.boot_retry_after.is_none());
assert_eq!(queued_status_connector_ids(&simulator), vec![0, 1, 2]);
}
#[test]
fn rejected_boot_result_does_not_enqueue_initial_status_notifications() {
let mut simulator = simulator_for_tests();
simulator.connected = true;
simulator
.apply_call_result_context(
&PendingContext::Boot,
&json!({
"status": "Rejected",
"currentTime": now_timestamp(),
"interval": 10
}),
)
.expect("boot result");
assert_eq!(
simulator.boot_registration_status,
BootRegistrationStatus::Rejected
);
assert!(simulator.boot_retry_after.is_some());
assert!(
!simulator
.should_enqueue_boot_on_connect()
.expect("boot check")
);
assert!(simulator.queue.is_empty());
simulator
.handle_common_command(SimulatorCommand::Boot, true)
.expect("boot command");
assert!(simulator.queue.is_empty());
assert_eq!(
simulator.boot_registration_status,
BootRegistrationStatus::Rejected
);
}
#[test]
fn pending_boot_result_keeps_v1_6_registration_pending() {
let mut simulator = simulator_for_tests();
simulator
.apply_call_result_context(
&PendingContext::Boot,
&json!({
"status": "Pending",
"currentTime": now_timestamp(),
"interval": 10
}),
)
.expect("boot result");
assert_eq!(
simulator.boot_registration_status,
BootRegistrationStatus::Pending
);
assert!(simulator.boot_retry_after.is_some());
assert!(simulator.queue.is_empty());
}
#[test]
fn expired_boot_retry_interval_enqueues_v1_6_boot_retry() {
let mut simulator = simulator_for_tests();
simulator.connected = true;
simulator.boot_registration_status = BootRegistrationStatus::Pending;
simulator.boot_retry_after = Some(
Instant::now()
.checked_sub(Duration::from_secs(1))
.expect("retry instant"),
);
simulator.check_boot_retry();
assert_eq!(
simulator.boot_registration_status,
BootRegistrationStatus::AwaitingResponse
);
assert!(simulator.boot_retry_after.is_none());
assert_eq!(
simulator.queue.front().map(|call| call.action.as_str()),
Some("BootNotification")
);
}
#[test]
fn trigger_message_boot_bypasses_v1_6_retry_interval() {
let mut simulator = simulator_for_tests();
simulator.boot_registration_status = BootRegistrationStatus::Pending;
simulator.boot_retry_after = Some(Instant::now() + Duration::from_secs(10));
let status = simulator
.trigger_message_v1_6_standard(
crate::ocpp::TriggerMessage_V1_6::BootNotification,
None,
)
.expect("trigger boot");
assert_eq!(status, ResponseStatus::Accepted);
assert_eq!(
simulator.boot_registration_status,
BootRegistrationStatus::AwaitingResponse
);
assert!(simulator.boot_retry_after.is_none());
assert_eq!(
simulator.queue.front().map(|call| call.action.as_str()),
Some("BootNotification")
);
}
#[test]
fn accepted_boot_result_enqueues_v2_x_connector_statuses_only() {
for_each_v2_x_simulator(|_, mut simulator| {
simulator
.apply_call_result_context(
&PendingContext::Boot,
&json!({
"status": "Accepted",
"currentTime": now_timestamp()
}),
)
.expect("boot result");
assert_eq!(queued_status_evse_ids(&simulator), vec![1, 2]);
});
}
#[test]
fn v1_6_commands_wait_for_boot_acceptance() {
let mut simulator = simulator_for_tests();
simulator.connected = true;
simulator.boot_registration_status = BootRegistrationStatus::Pending;
simulator
.handle_common_command(
SimulatorCommand::StartTransaction {
connector: 1,
id_token: "TOKEN".to_string(),
},
true,
)
.expect("start command should be handled");
assert!(simulator.queue.is_empty());
assert!(
simulator
.connectors
.values()
.all(|connector| connector.transaction.is_none())
);
simulator
.apply_call_result_context(
&PendingContext::Boot,
&json!({
"status": "Accepted",
"currentTime": now_timestamp()
}),
)
.expect("boot result");
simulator.queue.clear();
simulator
.handle_common_command(
SimulatorCommand::StartTransaction {
connector: 1,
id_token: "TOKEN".to_string(),
},
true,
)
.expect("start command should be handled after boot");
assert!(
simulator
.connectors
.get(&1)
.and_then(|state| state.transaction.as_ref())
.is_some()
);
assert!(
simulator
.queue
.iter()
.any(|call| call.action == "StartTransaction")
);
}
#[test]
fn v2_x_commands_wait_for_boot_acceptance() {
for_each_v2_x_simulator(|_, mut simulator| {
simulator.connected = true;
simulator.boot_registration_status = BootRegistrationStatus::Pending;
simulator
.handle_common_command(
SimulatorCommand::StartTransaction {
connector: 1,
id_token: "TOKEN".to_string(),
},
true,
)
.expect("start command should be handled");
assert!(simulator.queue.is_empty());
assert!(
simulator
.connectors
.values()
.all(|connector| connector.transaction.is_none())
);
simulator
.apply_call_result_context(
&PendingContext::Boot,
&json!({
"status": "Accepted",
"currentTime": now_timestamp()
}),
)
.expect("boot result");
simulator.queue.clear();
simulator
.handle_common_command(
SimulatorCommand::StartTransaction {
connector: 1,
id_token: "TOKEN".to_string(),
},
true,
)
.expect("start command should be handled after boot");
assert!(
simulator
.connectors
.get(&1)
.and_then(|state| state.transaction.as_ref())
.is_some()
);
assert!(
simulator
.queue
.iter()
.any(|call| call.action == "TransactionEvent")
);
});
}
#[test]
fn change_availability_v2_x_updates_target_evse() {
for_each_v2_x_simulator(|_, mut simulator| {
let payload = json!({
"operationalStatus": "Inoperative",
"evse": { "id": 2 }
});
let status = simulator
.change_availability_v2_x(&payload)
.expect("request should succeed");
assert_eq!(status, ResponseStatus::Accepted);
assert_eq!(
simulator
.connectors
.get(&2)
.map(|item| item.status.display()),
Some("Unavailable")
);
});
}
fn queued_status_connector_ids(simulator: &Simulator) -> Vec<u64> {
simulator
.queue
.iter()
.filter(|call| call.action == "StatusNotification")
.map(|call| call.payload["connectorId"].as_u64().expect("connector ID"))
.collect()
}
fn queued_status_evse_ids(simulator: &Simulator) -> Vec<u64> {
simulator
.queue
.iter()
.filter(|call| call.action == "StatusNotification")
.map(|call| call.payload["evseId"].as_u64().expect("evse ID"))
.collect()
}
#[test]
fn reserve_and_cancel_v2_x_auto_selects_available_evse() {
for_each_v2_x_simulator(|_, mut simulator| {
let reserve_status = simulator
.reserve_now_v2_x(&json!({
"id": 77,
"expiryDateTime": now_timestamp(),
"idToken": {
"idToken": "TOKEN",
"type": "Central"
}
}))
.expect("reserve should succeed");
assert_eq!(reserve_status, ResponseStatus::Accepted);
assert_eq!(simulator.reservations.get(&77), Some(&1));
assert_eq!(
simulator
.connectors
.get(&1)
.map(|item| item.status.display()),
Some("Reserved")
);
let cancel_status = simulator
.cancel_reservation_v2_x(&json!({ "reservationId": 77 }))
.expect("cancel should succeed");
assert_eq!(cancel_status, ResponseStatus::Accepted);
assert!(!simulator.reservations.contains_key(&77));
assert_eq!(
simulator
.connectors
.get(&1)
.map(|item| item.status.display()),
Some("Available")
);
});
}
#[test]
fn send_local_list_v2_x_updates_version() {
for_each_v2_x_simulator(|_, mut simulator| {
let payload = json!({ "versionNumber": 7, "updateType": "Full" });
let status = simulator
.send_local_list_v2_x(&payload)
.expect("send local list should parse");
assert_eq!(status, ResponseStatus::Accepted);
assert_eq!(simulator.local_auth_list_version, 7);
});
}
#[test]
fn disconnect_clears_pending_queue_and_status() {
let mut simulator = simulator_for_tests();
simulator.connected = true;
simulator.enqueue_heartbeat();
let call = simulator.queue.front().expect("queued call").clone();
simulator.pending = Some(PendingCall {
message_id: "pending".to_string(),
sent_at: Instant::now(),
call,
});
simulator.handle_disconnect("lost connection");
assert!(!simulator.connected);
assert!(simulator.pending.is_none());
assert!(simulator.queue.is_empty());
}
#[test]
fn repeated_pending_timeouts_unblock_queued_calls() {
let mut simulator = simulator_for_tests();
simulator.config.request_timeout = std::time::Duration::from_millis(1);
simulator.enqueue_heartbeat();
simulator
.enqueue_data_transfer("ocppsim", Some("Message"), Some("hello"))
.expect("data transfer should validate");
simulator
.enqueue_authorize("TOKEN".to_string())
.expect("authorize should validate");
let mut timed_out_actions = Vec::new();
while let Some(call) = simulator.queue.pop_front() {
timed_out_actions.push(call.action.clone());
simulator.pending = Some(PendingCall {
message_id: format!("timeout-{}", timed_out_actions.len()),
sent_at: Instant::now()
.checked_sub(std::time::Duration::from_secs(1))
.expect("past instant"),
call,
});
simulator.check_pending_timeout();
assert!(simulator.pending.is_none());
}
assert_eq!(
timed_out_actions,
vec![
"Heartbeat".to_string(),
"DataTransfer".to_string(),
"Authorize".to_string(),
]
);
}
#[test]
fn start_transaction_rejects_unstartable_connectors() {
let mut simulator = simulator_for_tests();
simulator.connector_mut(1).expect("connector").status =
ConnectorStatus::Unavailable;
assert!(
simulator
.start_transaction(1, "TOKEN".to_string(), false, None, false)
.is_err()
);
simulator.connector_mut(1).expect("connector").status =
ConnectorStatus::Faulted;
assert!(
simulator
.start_transaction(1, "TOKEN".to_string(), false, None, false)
.is_err()
);
let reserve_payload = json!({
"connectorId": 2,
"expiryDate": now_timestamp(),
"idTag": "TOKEN",
"reservationId": 7
});
assert_eq!(
simulator
.reserve_now_v1_6(&reserve_payload)
.expect("reserve connector"),
ResponseStatus::Accepted
);
assert!(
simulator
.start_transaction(2, "TOKEN".to_string(), false, None, false)
.is_err()
);
assert!(
simulator
.connectors
.values()
.all(|connector| connector.transaction.is_none())
);
}