use std::collections::HashMap;
use bytes::Bytes;
use futures::{SinkExt as _, StreamExt as _};
use map_core::client::MapClient;
use map_core::messages::{MessageEntry, ReadStatus};
use map_core::BMessage;
use tokio::io::{duplex, DuplexStream};
use super::models::{Direction, LiveMessage};
use super::{
accumulate, direction_of, folders, keep, peer_address, read_status_for, to_live_body, window,
ListFilter,
};
use crate::util::datetime_to_ms;
const CONNECT_RSP: &[u8] = include_bytes!("../../../imsg-obex/tests/fixtures/connect_rsp.bin");
const TELECOM_RSP: &[u8] =
include_bytes!("../../../imsg-obex/tests/fixtures/setpath_telecom_rsp.bin");
const MSG_RSP: &[u8] = include_bytes!("../../../imsg-obex/tests/fixtures/setpath_msg_rsp.bin");
const FOLDER_LISTING_RSP: &[u8] =
include_bytes!("../../../imsg-obex/tests/fixtures/get_folder_listing_000_rsp.bin");
async fn fake_folders_client(listing: &'static [u8]) -> anyhow::Result<MapClient<DuplexStream>> {
let (client_io, server_io) = duplex(4096);
tokio::spawn(async move {
let mut srv = obex_core::wrap(server_io);
let _ = srv.next().await;
let _ = srv.send(Bytes::from_static(CONNECT_RSP)).await;
for rsp in [TELECOM_RSP, MSG_RSP, listing] {
let _ = srv.next().await;
let _ = srv.send(Bytes::copy_from_slice(rsp)).await;
}
});
Ok(MapClient::connect(client_io).await?)
}
#[tokio::test]
async fn folders_returns_listing_in_device_document_order() -> anyhow::Result<()> {
let mut client = fake_folders_client(FOLDER_LISTING_RSP).await?;
let names: Vec<String> = folders(&mut client).await?.into_iter().map(|f| f.name).collect();
assert_eq!(names, ["inbox", "sent", "outbox", "deleted"]);
Ok(())
}
fn entry(sent: bool, read: bool, datetime: &str, sender: &str, recipient: &str) -> MessageEntry {
MessageEntry {
handle: "h".to_owned(),
subject: String::new(),
datetime: datetime.to_owned(),
sender_name: String::new(),
sender_addressing: sender.to_owned(),
recipient_name: String::new(),
recipient_addressing: recipient.to_owned(),
msg_type: "SMS_GSM".to_owned(),
size: 2,
read,
sent,
}
}
fn msg(ts: i64, address: &str, read: bool) -> LiveMessage {
LiveMessage {
handle: "h".to_owned(),
timestamp_ms: ts,
address: address.to_owned(),
folder: "inbox".to_owned(),
read,
text: "t".to_owned(),
}
}
#[test]
fn direction_sent_for_sent_and_outbox_folders() {
assert_eq!(direction_of("telecom/msg/sent"), Direction::Sent);
assert_eq!(direction_of("telecom/msg/outbox"), Direction::Sent);
}
#[test]
fn direction_received_for_inbox_and_deleted() {
assert_eq!(direction_of("telecom/msg/inbox"), Direction::Received);
assert_eq!(direction_of("telecom/msg/deleted"), Direction::Received);
}
#[test]
fn read_status_for_unread_filter_requests_unread_only() {
let f = ListFilter { unread: true, ..Default::default() };
assert_eq!(read_status_for(&f), Some(ReadStatus::Unread));
}
#[test]
fn read_status_for_no_filter_requests_everything() {
let f = ListFilter::default();
assert_eq!(read_status_for(&f), None);
}
#[test]
fn keep_matches_from_address_exactly() {
let f = ListFilter { from: Some("+1".to_owned()), ..Default::default() };
assert!(keep(&msg(0, "+1", false), &f));
assert!(!keep(&msg(0, "+2", false), &f));
}
#[test]
fn keep_drops_messages_before_since() {
let f = ListFilter { since_ms: Some(100), ..Default::default() };
assert!(!keep(&msg(99, "a", false), &f));
assert!(keep(&msg(100, "a", false), &f));
}
#[test]
fn window_applies_offset_then_limit() -> anyhow::Result<()> {
let msgs = vec![msg(3, "a", false), msg(2, "a", false), msg(1, "a", false)];
let f = ListFilter { offset: 1, limit: Some(1), ..Default::default() };
let out = window(msgs, &f);
assert_eq!(out.len(), 1);
let first = out.first().ok_or_else(|| anyhow::anyhow!("empty window"))?;
assert_eq!(first.timestamp_ms, 2);
Ok(())
}
#[test]
fn window_no_limit_keeps_rest_after_offset() {
let msgs = vec![msg(2, "a", false), msg(1, "a", false)];
let f = ListFilter { offset: 1, ..Default::default() };
assert_eq!(window(msgs, &f).len(), 1);
}
#[test]
fn peer_address_resolves_sender_for_received_recipient_for_sent() {
assert_eq!(peer_address(&entry(false, true, "", "+1", "+2")), "+1");
assert_eq!(peer_address(&entry(true, true, "", "+1", "+2")), "+2");
}
#[test]
fn accumulate_groups_peer_counts_total_unread_and_latest() -> anyhow::Result<()> {
let mut acc: HashMap<String, _> = HashMap::new();
accumulate(&mut acc, &entry(false, false, "20260101T000000", "+1", ""));
accumulate(&mut acc, &entry(true, true, "20260102T000000", "", "+1"));
let t = acc.get("+1").ok_or_else(|| anyhow::anyhow!("missing thread"))?;
assert_eq!(t.total, 2);
assert_eq!(t.unread, 1);
assert_eq!(Some(t.latest_ms), datetime_to_ms("20260102T000000"));
Ok(())
}
#[test]
fn accumulate_skips_empty_address() {
let mut acc = HashMap::new();
accumulate(&mut acc, &entry(false, false, "20260101T000000", "", ""));
assert!(acc.is_empty());
}
#[test]
fn to_live_body_sent_uses_recipient_and_outbox_direction() {
let bmsg = BMessage::outbound_sms("+15550002", "hi");
let body = to_live_body("hh".to_owned(), &bmsg);
assert_eq!(body.handle, "hh");
assert_eq!(body.direction, Direction::Sent);
assert_eq!(body.address, "+15550002");
assert!(!body.read);
assert_eq!(body.text, "hi");
}
#[test]
fn to_live_body_received_uses_originator_and_inbox_direction() -> anyhow::Result<()> {
let wire = BMessage::outbound_sms("+15550002", "hi")
.encode()
.replace("FOLDER:telecom/msg/outbox", "FOLDER:telecom/msg/inbox")
.replace("STATUS:UNREAD", "STATUS:READ")
.replace("TEL:\r\n", "TEL:+15550001\r\n");
let bmsg = BMessage::parse(&wire)?;
let body = to_live_body("hh".to_owned(), &bmsg);
assert_eq!(body.direction, Direction::Received);
assert_eq!(body.address, "+15550001");
assert!(body.read);
assert_eq!(body.text, "hi");
Ok(())
}