chat-system 0.1.3

A multi-protocol async chat crate — single interface for IRC, Matrix, Discord, Telegram, Slack, Signal, WhatsApp, and more
#![cfg(feature = "signal-cli")]

use chat_system::Messenger;
use chat_system::messengers::SignalCliMessenger;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};

#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;

fn unique_test_dir() -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    std::env::temp_dir().join(format!("chat-system-signal-cli-{nanos}"))
}

fn write_mock_signal_cli() -> (PathBuf, PathBuf) {
    let dir = unique_test_dir();
    fs::create_dir_all(&dir).unwrap();

    let script_path = dir.join("signal-cli-mock.sh");
    let log_path = dir.join("signal-cli.log");
    let script = r#"#!/bin/sh
set -eu
LOG_FILE="__LOG_PATH__"
if [ "${1:-}" = "--version" ]; then
    echo "signal-cli 0.13.0"
    exit 0
fi
if [ "${3:-}" = "send" ]; then
    echo "$*" >> "$LOG_FILE"
    exit 0
fi
if [ "${3:-}" = "receive" ]; then
    cat <<'EOF'
{"envelope":{"timestamp":1710000000000,"sourceNumber":"+15551234567","sourceName":"Alice","dataMessage":{"message":"hello from signal"}}}
{"envelope":{"timestamp":1710000001000,"sourceNumber":"+15557654321","dataMessage":{"message":"group hello","groupInfo":{"groupId":"group-123"},"quote":{"id":1709999999000}}}}
EOF
    exit 0
fi
echo "unexpected args: $*" >&2
exit 1
"#
        .replace("__LOG_PATH__", &log_path.display().to_string());

    fs::write(&script_path, script).unwrap();
    #[cfg(unix)]
    {
        let mut permissions = fs::metadata(&script_path).unwrap().permissions();
        permissions.set_mode(0o755);
        fs::set_permissions(&script_path, permissions).unwrap();
    }

    (script_path, log_path)
}

#[tokio::test]
async fn signal_initialize_sets_connected() {
    let (script_path, _) = write_mock_signal_cli();
    let mut messenger = SignalCliMessenger::new("signal", "+15550000000")
        .with_cli_path(script_path.to_string_lossy().into_owned());

    messenger.initialize().await.unwrap();

    assert!(messenger.is_connected());
}

#[tokio::test]
async fn signal_send_message_invokes_cli() {
    let (script_path, log_path) = write_mock_signal_cli();
    let mut messenger = SignalCliMessenger::new("signal", "+15550000000")
        .with_cli_path(script_path.to_string_lossy().into_owned());
    messenger.initialize().await.unwrap();

    let message_id = messenger
        .send_message("+15551112222", "test signal send")
        .await
        .unwrap();

    assert!(message_id.starts_with("signal:"));
    let log = fs::read_to_string(log_path).unwrap();
    assert!(log.contains("-u +15550000000 send -m test signal send +15551112222"));
}

#[tokio::test]
async fn signal_receive_messages_parses_json_output() {
    let (script_path, _) = write_mock_signal_cli();
    let mut messenger = SignalCliMessenger::new("signal", "+15550000000")
        .with_cli_path(script_path.to_string_lossy().into_owned());
    messenger.initialize().await.unwrap();

    let messages = messenger.receive_messages().await.unwrap();

    assert_eq!(messages.len(), 2);
    assert_eq!(messages[0].sender, "Alice");
    assert_eq!(messages[0].content, "hello from signal");
    assert_eq!(messages[0].timestamp, 1_710_000_000);
    assert!(messages[0].is_direct);
    assert_eq!(messages[1].sender, "+15557654321");
    assert_eq!(messages[1].channel.as_deref(), Some("group-123"));
    assert_eq!(messages[1].reply_to.as_deref(), Some("1709999999000"));
    assert!(!messages[1].is_direct);
}

#[tokio::test]
async fn signal_disconnect_clears_connected() {
    let (script_path, _) = write_mock_signal_cli();
    let mut messenger = SignalCliMessenger::new("signal", "+15550000000")
        .with_cli_path(script_path.to_string_lossy().into_owned());
    messenger.initialize().await.unwrap();

    messenger.disconnect().await.unwrap();

    assert!(!messenger.is_connected());
}