use super::*;
use bacnet_services::device_mgmt::ReinitializeDeviceRequest;
use bacnet_types::enums::ReinitializedState;
type Observed = Arc<StdMutex<Vec<Vec<Apdu>>>>;
type Answer = fn() -> Result<(), Error>;
const INVOKE_ID: u8 = 9;
fn request(state: ReinitializedState, password: Option<&str>) -> ConfirmedRequestPdu {
let mut data = BytesMut::new();
ReinitializeDeviceRequest {
reinitialized_state: state,
password: password.map(str::to_owned),
}
.encode(&mut data)
.unwrap();
ConfirmedRequestPdu {
segmented: false,
more_follows: false,
segmented_response_accepted: false,
max_segments: None,
max_apdu_length: 1476,
invoke_id: INVOKE_ID,
sequence_number: None,
proposed_window_size: None,
service_choice: ConfirmedServiceChoice::REINITIALIZE_DEVICE,
service_request: data.freeze(),
}
}
fn replies(sent: &StdMutex<Vec<Bytes>>) -> Vec<Apdu> {
sent.lock()
.unwrap()
.iter()
.map(|bytes| decode_apdu(decode_npdu(bytes.clone()).unwrap().payload).unwrap())
.collect()
}
fn install(f: &mut Fixture, outcome: Answer) -> Observed {
let observed = Observed::default();
let (seen, responses) = (Arc::clone(&observed), Arc::clone(&f.transport.responses));
f.server.config_mut().on_reinitialize = Some(Arc::new(move |context, _| {
let (seen, responses) = (Arc::clone(&seen), Arc::clone(&responses));
context.after_reply(move || seen.lock().unwrap().push(replies(&responses)));
outcome()
}));
observed
}
async fn over_the_link(f: &Fixture, request: ConfirmedRequestPdu) {
BACnetServer::handle_confirmed_request(
&f.server.test_services(),
&f.server.confirmed_request_tracker,
&f.server.request_tasks.spawner(),
SOURCE,
None,
request,
None,
)
.await;
}
fn is_ack(reply: &Apdu) -> bool {
matches!(reply, Apdu::SimpleAck(ack)
if ack.invoke_id == INVOKE_ID
&& ack.service_choice == ConfirmedServiceChoice::REINITIALIZE_DEVICE)
}
#[tokio::test(start_paused = true)]
async fn the_after_reply_hook_runs_once_the_simple_ack_is_on_the_wire() {
for state in [
ReinitializedState::COLDSTART,
ReinitializedState::WARMSTART,
ReinitializedState::START_BACKUP,
ReinitializedState::END_BACKUP,
ReinitializedState::START_RESTORE,
ReinitializedState::END_RESTORE,
ReinitializedState::ABORT_RESTORE,
ReinitializedState::ACTIVATE_CHANGES,
] {
let mut f = plain_server(reporter()).await;
let observed = install(&mut f, || Ok(()));
over_the_link(&f, request(state, None)).await;
let runs = observed.lock().unwrap().clone();
assert_eq!(runs.len(), 1, "{state:?}: the hook runs once");
assert!(
matches!(runs[0].as_slice(), [reply] if is_ack(reply)),
"{state:?}: the SimpleACK was on the wire before the hook ran: {runs:?}"
);
f.server.stop().await.unwrap();
}
}
#[tokio::test(start_paused = true)]
async fn the_after_reply_hook_follows_the_reply_channel_handoff() {
let mut f = plain_server(reporter()).await;
let (tx, rx) = oneshot::channel();
let rx = Arc::new(StdMutex::new(rx));
let handed_off = Arc::new(StdMutex::new(Vec::new()));
let (seen, waiting) = (Arc::clone(&handed_off), Arc::clone(&rx));
f.server.config_mut().on_reinitialize = Some(Arc::new(move |context, _| {
let (seen, waiting) = (Arc::clone(&seen), Arc::clone(&waiting));
context.after_reply(move || {
let reply = waiting.lock().unwrap().try_recv().ok();
seen.lock().unwrap().push(reply);
});
Ok(())
}));
BACnetServer::handle_confirmed_request(
&f.server.test_services(),
&f.server.confirmed_request_tracker,
&f.server.request_tasks.spawner(),
SOURCE,
None,
request(ReinitializedState::WARMSTART, None),
Some(tx),
)
.await;
let runs = handed_off.lock().unwrap().clone();
assert_eq!(runs.len(), 1);
let reply = runs[0].clone().expect("the reply was in the channel first");
assert!(is_ack(
&decode_apdu(decode_npdu(reply).unwrap().payload).unwrap()
));
f.server.stop().await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn the_after_reply_hook_never_runs_for_a_refusal() {
let cases: [(&str, Answer, Option<&str>, ErrorCode); 5] = [
(
"handler refusal",
|| {
Err(Error::protocol(
ErrorClass::DEVICE,
ErrorCode::CONFIGURATION_IN_PROGRESS,
))
},
None,
ErrorCode::CONFIGURATION_IN_PROGRESS,
),
(
"handler error",
|| Err(Error::Encoding("disk full".into())),
None,
ErrorCode::OTHER,
),
(
"handler panic",
|| panic!("handler panic after registering"),
None,
ErrorCode::OTHER,
),
(
"missing password",
|| Ok(()),
None,
ErrorCode::PASSWORD_FAILURE,
),
(
"wrong password",
|| Ok(()),
Some("wrong"),
ErrorCode::PASSWORD_FAILURE,
),
];
for (case, outcome, password, code) in cases {
let mut f = plain_server(reporter()).await;
let observed = install(&mut f, outcome);
if code == ErrorCode::PASSWORD_FAILURE {
f.server.config_mut().reinit_password = Some("required".into());
}
over_the_link(&f, request(ReinitializedState::WARMSTART, password)).await;
let sent = replies(&f.transport.responses);
assert!(
matches!(sent.as_slice(), [Apdu::Error(error)] if error.error_code == code),
"{case}: {sent:?}"
);
settle().await;
assert!(observed.lock().unwrap().is_empty(), "{case}: hook ran");
f.server.stop().await.unwrap();
}
}
#[tokio::test(start_paused = true)]
async fn the_after_reply_hook_never_runs_when_the_reply_is_not_handed_off() {
let mut f = plain_server(reporter()).await;
let observed = install(&mut f, || Ok(()));
f.transport.fail_response.store(true, Ordering::Release);
over_the_link(&f, request(ReinitializedState::COLDSTART, None)).await;
assert!(matches!(
replies(&f.transport.responses).as_slice(),
[reply] if is_ack(reply)
));
settle().await;
assert!(observed.lock().unwrap().is_empty());
f.server.stop().await.unwrap();
}