use crate::channels::whatsapp::interactive::{
Button, MAX_BUTTONS, build, parse_suggestion_tap, parse_tap, suggestion_card_fits,
};
fn card(msg: &waproto::whatsapp::Message) -> &waproto::whatsapp::message::InteractiveMessage {
msg.view_once_message
.as_option()
.expect("interactive messages must be wrapped in viewOnceMessage")
.message
.as_option()
.expect("wrapper carries the real message")
.interactive_message
.as_option()
.expect("card present")
}
fn flow(
msg: &waproto::whatsapp::Message,
) -> &waproto::whatsapp::message::interactive_message::NativeFlowMessage {
use waproto::whatsapp::message::interactive_message::InteractiveMessage as Oneof;
match card(msg).interactive_message.as_ref() {
Some(Oneof::NativeFlowMessage(f)) => f,
_ => panic!("expected a native-flow card"),
}
}
fn approval_buttons() -> Vec<Button> {
vec![
Button::new("wa_approve_yes", "Yes"),
Button::new("wa_approve_no", "No"),
]
}
#[test]
fn the_card_is_wrapped_in_view_once() {
let msg = build("approve?", None, &approval_buttons());
assert!(msg.view_once_message.is_set());
assert!(
msg.interactive_message.is_unset(),
"the card belongs inside the wrapper, not beside it"
);
}
#[test]
fn the_wrapper_declares_multi_device() {
let inner = build("approve?", None, &approval_buttons())
.view_once_message
.unwrap()
.message
.unwrap();
assert_eq!(
inner
.message_context_info
.as_option()
.and_then(|c| c.device_list_metadata_version),
Some(2)
);
}
#[test]
fn the_body_carries_the_full_prompt() {
let msg = build("Reply yes or no.", None, &approval_buttons());
assert_eq!(
card(&msg).body.as_option().and_then(|b| b.text.as_deref()),
Some("Reply yes or no.")
);
}
#[test]
fn a_footer_is_optional() {
let without = build("body", None, &approval_buttons());
assert!(card(&without).footer.is_unset());
let with = build("body", Some("tap or type"), &approval_buttons());
assert_eq!(
card(&with)
.footer
.as_option()
.and_then(|f| f.text.as_deref()),
Some("tap or type")
);
}
#[test]
fn every_button_is_a_quick_reply_carrying_its_id_and_label() {
let msg = build("body", None, &approval_buttons());
let buttons = &flow(&msg).buttons;
assert_eq!(buttons.len(), 2);
assert_eq!(buttons[0].name.as_deref(), Some("quick_reply"));
let params: serde_json::Value =
serde_json::from_str(buttons[0].button_params_json.as_deref().unwrap()).unwrap();
assert_eq!(params["id"], "wa_approve_yes");
assert_eq!(params["display_text"], "Yes");
}
#[test]
fn a_label_containing_a_quote_still_produces_valid_json() {
let msg = build("body", None, &[Button::new("id", r#"Say "yes""#)]);
let raw = flow(&msg).buttons[0].button_params_json.as_deref().unwrap();
let params: serde_json::Value = serde_json::from_str(raw).expect("valid JSON");
assert_eq!(params["display_text"], r#"Say "yes""#);
}
#[test]
fn buttons_past_the_cap_are_dropped_here_rather_than_on_the_wire() {
let many: Vec<Button> = (0..6)
.map(|i| Button::new(format!("id{i}"), format!("B{i}")))
.collect();
assert_eq!(flow(&build("body", None, &many)).buttons.len(), MAX_BUTTONS);
}
#[test]
fn a_card_with_no_buttons_is_still_well_formed() {
let msg = build("body only", None, &[]);
assert!(flow(&msg).buttons.is_empty());
}
#[test]
fn a_native_flow_tap_yields_its_id() {
use waproto::whatsapp::message::InteractiveResponseMessage;
use waproto::whatsapp::message::interactive_response_message::{
InteractiveResponseMessage as Oneof, NativeFlowResponseMessage,
};
let msg = waproto::whatsapp::Message {
interactive_response_message: InteractiveResponseMessage {
interactive_response_message: Some(Oneof::NativeFlowResponseMessage(Box::new(
NativeFlowResponseMessage {
name: Some("quick_reply".to_string()),
params_json: Some(
r#"{"display_text":"Yes","id":"wa_approve_yes"}"#.to_string(),
),
..Default::default()
},
))),
..Default::default()
}
.into(),
..Default::default()
};
assert_eq!(parse_tap(&msg).as_deref(), Some("wa_approve_yes"));
}
#[test]
fn a_legacy_buttons_tap_is_still_read() {
let msg = waproto::whatsapp::Message {
buttons_response_message: waproto::whatsapp::message::ButtonsResponseMessage {
selected_button_id: Some("wa_approve_no".to_string()),
..Default::default()
}
.into(),
..Default::default()
};
assert_eq!(parse_tap(&msg).as_deref(), Some("wa_approve_no"));
}
#[test]
fn an_ordinary_message_is_not_a_tap() {
let msg = waproto::whatsapp::Message {
conversation: Some("yes".to_string()),
..Default::default()
};
assert_eq!(parse_tap(&msg), None);
}
#[test]
fn unreadable_tap_params_yield_nothing_rather_than_panicking() {
use waproto::whatsapp::message::InteractiveResponseMessage;
use waproto::whatsapp::message::interactive_response_message::{
InteractiveResponseMessage as Oneof, NativeFlowResponseMessage,
};
for params in ["not json at all", "{}", r#"{"id":42}"#, r#"{"other":"x"}"#] {
let msg = waproto::whatsapp::Message {
interactive_response_message: InteractiveResponseMessage {
interactive_response_message: Some(Oneof::NativeFlowResponseMessage(Box::new(
NativeFlowResponseMessage {
params_json: Some(params.to_string()),
..Default::default()
},
))),
..Default::default()
}
.into(),
..Default::default()
};
assert_eq!(parse_tap(&msg), None, "params: {params}");
}
}
#[test]
fn interactive_buttons_are_off_by_default() {
assert!(
!crate::config::Config::default()
.channels
.whatsapp
.interactive_buttons
);
}
#[test]
fn suggestion_taps_decode_to_their_number() {
assert_eq!(parse_suggestion_tap("wa_suggest_1"), Some(1));
assert_eq!(parse_suggestion_tap("wa_suggest_3"), Some(3));
assert_eq!(parse_suggestion_tap("wa_suggest_0"), None);
assert_eq!(parse_suggestion_tap("wa_suggest_"), None);
assert_eq!(parse_suggestion_tap("wa_suggest_x"), None);
assert_eq!(parse_suggestion_tap("wa_approve_yes"), None);
assert_eq!(parse_suggestion_tap(""), None);
}
#[test]
fn suggestion_cards_only_fit_within_the_button_cap() {
assert!(!suggestion_card_fits(0));
assert!(suggestion_card_fits(1));
assert!(suggestion_card_fits(MAX_BUTTONS));
assert!(!suggestion_card_fits(MAX_BUTTONS + 1));
}