use assert_cmd::Command;
use predicates::prelude::*;
use predicates::str::contains;
use serial_test::serial;
mod common;
use common::contains_path;
fn bin() -> Command {
let mut c = Command::cargo_bin("zad").expect("zad binary built");
c.env("ZAD_SECRETS_MEMORY", "1");
c
}
fn seed_global(home: &std::path::Path) {
seed_global_with_scopes(home, &["chats", "messages.read", "messages.send"]);
}
fn seed_global_with_scopes(home: &std::path::Path, scopes: &[&str]) {
let p = home
.join(".zad")
.join("services")
.join("telegram")
.join("config.toml");
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
let scope_list = scopes
.iter()
.map(|s| format!("\"{s}\""))
.collect::<Vec<_>>()
.join(", ");
std::fs::write(&p, format!("scopes = [{scope_list}]\n")).unwrap();
}
fn enable_telegram(home: &std::path::Path, project: &std::path::Path) {
bin()
.env("ZAD_HOME_OVERRIDE", home)
.current_dir(project)
.args(["service", "enable", "telegram"])
.assert()
.success();
}
#[test]
fn help_lists_every_subcommand() {
bin()
.args(["telegram", "--help"])
.assert()
.success()
.stdout(contains("send"))
.stdout(contains("read"))
.stdout(contains("listen"))
.stdout(contains("chats"))
.stdout(contains("discover"))
.stdout(contains("directory"))
.stdout(contains("permissions"))
.stdout(contains("self"));
}
#[test]
#[serial]
fn send_fails_when_project_not_enabled() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global(home.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "send", "--chat", "12345", "hello"])
.assert()
.failure()
.stderr(contains("telegram is not enabled for this project"));
}
#[test]
#[serial]
fn read_fails_when_project_not_enabled() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global(home.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "read", "--chat", "12345"])
.assert()
.failure()
.stderr(contains("telegram is not enabled for this project"));
}
#[test]
#[serial]
fn chats_fails_when_credentials_missing() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global(home.path());
enable_telegram(home.path(), project.path());
std::fs::remove_file(
home.path()
.join(".zad")
.join("services")
.join("telegram")
.join("config.toml"),
)
.unwrap();
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "chats"])
.assert()
.failure()
.stderr(contains("no Telegram credentials found"));
}
#[test]
#[serial]
fn send_requires_chat_or_default() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global(home.path());
enable_telegram(home.path(), project.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "send", "hello"])
.assert()
.failure()
.stderr(contains("no chat specified"));
}
#[test]
#[serial]
fn send_rejects_unknown_alias() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global(home.path());
enable_telegram(home.path(), project.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "send", "--chat", "not-a-chat", "hi"])
.assert()
.failure()
.stderr(contains("is neither a chat_id nor a known directory entry"));
}
#[test]
#[serial]
fn send_requires_body_or_stdin() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global(home.path());
enable_telegram(home.path(), project.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "send", "--chat", "12345"])
.assert()
.failure()
.stderr(contains("missing message body"));
}
#[test]
#[serial]
fn send_rejects_oversized_body() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global(home.path());
enable_telegram(home.path(), project.path());
let body = "x".repeat(4097);
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "send", "--chat", "12345", &body])
.assert()
.failure()
.stderr(contains("4097 characters").and(contains("hard limit is 4096")));
}
#[test]
#[serial]
fn read_rejects_zero_limit() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global(home.path());
enable_telegram(home.path(), project.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "read", "--chat", "12345", "--limit", "0"])
.assert()
.failure()
.stderr(contains("--limit"));
}
#[test]
#[serial]
fn read_rejects_limit_above_100() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global(home.path());
enable_telegram(home.path(), project.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "read", "--chat", "12345", "--limit", "101"])
.assert()
.failure()
.stderr(contains("between 1 and 100"));
}
#[test]
#[serial]
fn send_denied_when_scope_missing() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global_with_scopes(home.path(), &["messages.read", "chats"]);
enable_telegram(home.path(), project.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "send", "--chat", "12345", "hi"])
.assert()
.failure()
.stderr(
contains("scope `messages.send` is not enabled")
.and(contains_path("services/telegram/config.toml")),
);
}
#[test]
#[serial]
fn read_denied_when_scope_missing() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global_with_scopes(home.path(), &["messages.send", "chats"]);
enable_telegram(home.path(), project.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "read", "--chat", "12345"])
.assert()
.failure()
.stderr(contains("scope `messages.read` is not enabled"));
}
#[test]
#[serial]
fn chats_denied_when_chats_scope_missing() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global_with_scopes(home.path(), &["messages.read", "messages.send"]);
enable_telegram(home.path(), project.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "chats"])
.assert()
.failure()
.stderr(contains("scope `chats` is not enabled"));
}
#[test]
#[serial]
fn discover_denied_when_chats_scope_missing() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global_with_scopes(home.path(), &["messages.read", "messages.send"]);
enable_telegram(home.path(), project.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "discover"])
.assert()
.failure()
.stderr(contains("scope `chats` is not enabled"));
}
#[test]
#[serial]
fn listen_fails_when_project_not_enabled() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global(home.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "listen", "--chat", "12345"])
.assert()
.failure()
.stderr(contains("telegram is not enabled for this project"));
}
#[test]
#[serial]
fn listen_denied_when_scope_missing() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global_with_scopes(home.path(), &["messages.send", "chats"]);
enable_telegram(home.path(), project.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "listen", "--chat", "12345"])
.assert()
.failure()
.stderr(contains("scope `messages.read` is not enabled"));
}
#[test]
#[serial]
fn listen_rejects_unknown_alias() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global(home.path());
enable_telegram(home.path(), project.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "listen", "--chat", "not-a-chat"])
.assert()
.failure()
.stderr(contains("is neither a chat_id nor a known directory entry"));
}
#[test]
#[serial]
fn listen_rejects_timeout_above_50() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global(home.path());
enable_telegram(home.path(), project.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "listen", "--chat", "12345", "--timeout", "51"])
.assert()
.failure()
.stderr(contains("--timeout"));
}
#[test]
#[serial]
fn listen_rejects_zero_timeout() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global(home.path());
enable_telegram(home.path(), project.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "listen", "--chat", "12345", "--timeout", "0"])
.assert()
.failure()
.stderr(contains("--timeout"));
}
#[test]
#[serial]
fn listen_permission_denied_names_listen_function() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global(home.path());
enable_telegram(home.path(), project.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "permissions", "init"])
.assert()
.success();
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args([
"telegram",
"permissions",
"add",
"--function",
"listen",
"--target",
"chat",
"--list",
"deny",
"*",
])
.assert()
.success();
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "permissions", "commit"])
.assert()
.success();
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "listen", "--chat", "12345"])
.assert()
.failure()
.stderr(
contains("permission denied")
.and(contains("listen"))
.and(contains_path("services/telegram/permissions.toml")),
);
}
#[test]
fn telegram_without_subcommand_errors() {
bin()
.args(["telegram"])
.assert()
.failure()
.stderr(contains("missing subcommand"));
}
#[test]
fn send_help_lists_dry_run_flag() {
bin()
.args(["telegram", "send", "--help"])
.assert()
.success()
.stdout(contains("--dry-run"));
}
#[test]
#[serial]
fn send_dry_run_previews_without_token() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global(home.path());
enable_telegram(home.path(), project.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args([
"telegram",
"send",
"--chat",
"12345",
"--dry-run",
"hello world",
])
.assert()
.success()
.stdout(contains("telegram.send"))
.stdout(contains("\"body\": \"hello world\""))
.stdout(contains("\"chat_id\": \"12345\""));
}
#[test]
#[serial]
fn send_dry_run_still_enforces_scope() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global_with_scopes(home.path(), &["messages.read", "chats"]);
enable_telegram(home.path(), project.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "send", "--chat", "12345", "--dry-run", "hello"])
.assert()
.failure()
.stderr(contains("scope `messages.send` is not enabled"));
}
#[test]
#[serial]
fn send_dry_run_still_rejects_oversized_body() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global(home.path());
enable_telegram(home.path(), project.path());
let body = "x".repeat(4097);
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "send", "--chat", "12345", "--dry-run", &body])
.assert()
.failure()
.stderr(contains("4097 characters").and(contains("hard limit is 4096")));
}
#[test]
#[serial]
fn send_dry_run_does_not_print_sent_line() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global(home.path());
enable_telegram(home.path(), project.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["telegram", "send", "--chat", "12345", "--dry-run", "hi"])
.assert()
.success()
.stdout(contains("Sent message").not());
}
#[test]
#[serial]
fn send_dry_run_resolves_directory_alias() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global(home.path());
enable_telegram(home.path(), project.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args([
"telegram",
"directory",
"set",
"team-room",
"--",
"-1001234567890",
])
.assert()
.success();
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args([
"telegram",
"send",
"--chat",
"team-room",
"--dry-run",
"ship it",
])
.assert()
.success()
.stdout(contains("\"chat_id\": \"-1001234567890\""));
}