Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 4 additions & 222 deletions whatsrust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
//! Public facade for the WhatsRust bridge.
//! Implementation and focused tests live in owning modules.
//! Explicit reexports keep the public API boundary auditable.

use std::ffi::CString;

#[macro_use]
Expand Down Expand Up @@ -45,228 +49,6 @@ pub use registrations::{
set_log_handler, set_message_handler, set_optimistic_text_sent_handler, set_presence_handler,
};

#[cfg(test)]
mod file_kind_tests {
use super::{FileKind, file_kind_discriminant};

#[test]
fn ffi_file_kind_discriminants_remain_stable_for_all_media_types() {
let cases = [
(FileKind::Image, 0),
(FileKind::Video, 1),
(FileKind::Audio, 2),
(FileKind::Document, 3),
(FileKind::Sticker, 4),
];

for (kind, expected) in cases {
assert_eq!(file_kind_discriminant(&kind), expected);
}
}
}

#[cfg(test)]
mod message_action_tests {
use std::ffi::CString;

use super::actions::{edit_to_ffi, reaction_to_ffi, revoke_to_ffi};
use super::events::{message_action_event_from_ffi, reaction_event_from_ffi};
use super::{CMessageActionEvent, CReactionEvent, Event, JID, MessageActionKind};

#[test]
fn reaction_mapping_preserves_every_ordinary_message_field() {
let chat = JID::from("1234567890@s.whatsapp.net".to_owned());
let sender = JID::from("0987654321@s.whatsapp.net".to_owned());
let message_id = "message".into();
let (target, destination, sender, id, reaction) =
reaction_to_ffi(&chat, &chat, &sender, &message_id, "👍").unwrap();
assert_eq!(target.to_str().unwrap(), "1234567890@s.whatsapp.net");
assert_eq!(destination.to_str().unwrap(), "1234567890@s.whatsapp.net");
assert_eq!(chat.0.as_ref(), "1234567890@s.whatsapp.net");
assert_eq!(sender.to_str().unwrap(), "0987654321@s.whatsapp.net");
assert_eq!(id.to_str().unwrap(), "message");
assert_eq!(reaction.to_str().unwrap(), "👍");
}

#[test]
fn reaction_mapping_rejects_nul_before_the_ffi_boundary() {
let jid = JID::from("1234567890@s.whatsapp.net".to_owned());
let invalid_id = "message\0id".into();
let message_id = "message".into();

assert!(reaction_to_ffi(&jid, &jid, &jid, &invalid_id, "👍").is_err());
assert!(reaction_to_ffi(&jid, &jid, &jid, &message_id, "👍\0").is_err());
}

#[test]
fn reaction_event_copies_reactor_and_empty_removal_payload() {
let chat = CString::new("group@g.us").unwrap();
let target = CString::new("target").unwrap();
let reactor = CString::new("reactor@s.whatsapp.net").unwrap();
for (text, expected) in [("👍", "👍"), ("", "")] {
let text = CString::new(text).unwrap();
let event = CReactionEvent {
chat: chat.as_ptr(),
target_message_id: target.as_ptr(),
participant: reactor.as_ptr(),
text: text.as_ptr(),
is_from_me: false,
};
let Event::Reaction {
chat,
target_message_id,
participant,
text,
is_from_me,
} = (unsafe { reaction_event_from_ffi(&event) })
else {
panic!("expected reaction event");
};
assert_eq!(chat.0.as_ref(), "group@g.us");
assert_eq!(target_message_id.as_ref(), "target");
assert_eq!(participant.0.as_ref(), "reactor@s.whatsapp.net");
assert_eq!(text.as_ref(), expected);
assert!(!is_from_me);
}
}

#[test]
fn edit_mapping_preserves_fields_and_rejects_invalid_replacements() {
let input_chat = JID::from("1234567890@s.whatsapp.net".to_owned());
let message_id = "message".into();
let (chat, id, replacement) = edit_to_ffi(&input_chat, &message_id, "replacement").unwrap();

assert_eq!(chat.to_str().unwrap(), "1234567890@s.whatsapp.net");
assert_eq!(id.to_str().unwrap(), "message");
assert_eq!(replacement.to_str().unwrap(), "replacement");
assert!(edit_to_ffi(&input_chat, &message_id, "").is_err());
assert!(edit_to_ffi(&input_chat, &message_id, " \t").is_err());
assert!(edit_to_ffi(&input_chat, &"message\0id".into(), "replacement").is_err());
}

#[test]
fn revoke_mapping_preserves_every_field() {
let chat = JID::from("1234567890@s.whatsapp.net".to_owned());
let sender = JID::from("0987654321@s.whatsapp.net".to_owned());
let message_id: super::MessageId = "message".into();
let (cchat, csender, cid) = revoke_to_ffi(&chat, &sender, &message_id).unwrap();

assert_eq!(cchat.to_str().unwrap(), "1234567890@s.whatsapp.net");
assert_eq!(csender.to_str().unwrap(), "0987654321@s.whatsapp.net");
assert_eq!(cid.to_str().unwrap(), "message");
}

#[test]
fn revoke_mapping_rejects_nul_before_the_ffi_boundary() {
let jid = JID::from("1234567890@s.whatsapp.net".to_owned());
let invalid_id: super::MessageId = "message\0id".into();
let valid_id: super::MessageId = "message".into();

assert!(revoke_to_ffi(&jid, &jid, &invalid_id).is_err());
assert!(revoke_to_ffi(&jid, &jid, &valid_id).is_ok());
}

#[test]
fn message_action_event_copies_ffi_buffers_before_go_releases_them() {
let action_id = CString::new("edit-1").unwrap();
let chat = CString::new("chat@example.test").unwrap();
let sender = CString::new("sender@example.test").unwrap();
let target = CString::new("target").unwrap();
let replacement = CString::new("new body").unwrap();
let event = CMessageActionEvent {
action_id: action_id.as_ptr(),
chat: chat.as_ptr(),
sender: sender.as_ptr(),
target_message_id: target.as_ptr(),
replacement: replacement.as_ptr(),
occurred_at: 42,
arrival_order: 7,
kind: 0,
};

let Event::MessageAction {
action_id,
target_message_id,
chat,
sender,
kind,
occurred_at,
arrival_order,
} = (unsafe { message_action_event_from_ffi(&event) })
else {
panic!("expected message action");
};
assert_eq!(action_id.as_ref(), "edit-1");
assert_eq!(target_message_id.as_ref(), "target");
assert_eq!(chat.0.as_ref(), "chat@example.test");
assert_eq!(sender.0.as_ref(), "sender@example.test");
assert_eq!(
kind,
MessageActionKind::Edit {
replacement: "new body".into()
}
);
assert_eq!((occurred_at, arrival_order), (42, 7));
}
}

#[cfg(test)]
mod presence_event_tests {
use std::{
ffi::CString,
sync::{Mutex, mpsc},
};

use super::{
C_TestEmitPresenceEvent, C_TestEmitPresenceEventsConcurrently, PresenceUpdate,
set_presence_handler,
};

static PRESENCE_HANDLER_TEST_LOCK: Mutex<()> = Mutex::new(());

#[test]
fn go_trampoline_reaches_dedicated_rust_presence_handler() {
let _guard = PRESENCE_HANDLER_TEST_LOCK.lock().unwrap();
let (sender, receiver) = mpsc::channel();
set_presence_handler(move |update| sender.send(update).unwrap());
let from = CString::new("alice@s.whatsapp.net").unwrap();

unsafe { C_TestEmitPresenceEvent(from.as_ptr(), true, 42) };

let PresenceUpdate {
from,
unavailable,
last_seen,
} = receiver.recv().unwrap();
assert_eq!(from.0.as_ref(), "alice@s.whatsapp.net");
assert!(unavailable);
assert_eq!(last_seen, 42);
assert!(receiver.try_recv().is_err());
}

#[test]
fn concurrent_go_goroutines_reach_dedicated_rust_presence_handler() {
let _guard = PRESENCE_HANDLER_TEST_LOCK.lock().unwrap();
const COUNT: u32 = 64;
let (sender, receiver) = mpsc::channel();
set_presence_handler(move |update| sender.send(update).unwrap());
let from = CString::new("alice@s.whatsapp.net").unwrap();

unsafe { C_TestEmitPresenceEventsConcurrently(from.as_ptr(), COUNT) };

let mut last_seen = (0..COUNT as i64).collect::<Vec<_>>();
let mut received = (0..COUNT)
.map(|_| receiver.recv().unwrap())
.collect::<Vec<_>>();
received.sort_by_key(|update| update.last_seen);
for (update, expected) in received.iter().zip(last_seen.drain(..)) {
assert_eq!(update.from.0.as_ref(), "alice@s.whatsapp.net");
assert!(!update.unavailable);
assert_eq!(update.last_seen, expected);
}
}
}

pub use caches::{forward_source, message_mention_ranges, message_push_name};
pub use caches::{
remove_forward_source, store_forward_source, store_message_mention_ranges,
Expand Down
Loading