ocppsim 0.1.1

A terminal OCPP-J charge point simulator.
Documentation
use std::time::Instant;

use serde_json::json;

use super::*;

#[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 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 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".to_string()), false, false)
    .expect("stop should succeed");
  assert_eq!(
    simulator
      .connectors
      .get(&1)
      .map(|item| item.status.display()),
    Some("Unavailable")
  );
}

#[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_v2_x_updates_target_evse() {
  let mut simulator = simulator_for_tests_v2_0_1();
  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")
  );
}

#[test]
fn reserve_and_cancel_v2_x_auto_selects_available_evse() {
  let mut simulator = simulator_for_tests_v2_0_1();
  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() {
  let mut simulator = simulator_for_tests_v2_0_1();
  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".to_string(),
    Some("Message".to_string()),
    Some("hello".to_string()),
  );
  simulator.enqueue_authorize("TOKEN".to_string());

  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())
  );
}