use assert_cmd::Command;
use predicates::prelude::*;
use predicates::str::contains;
use serial_test::serial;
mod common;
fn bin() -> Command {
let mut c = Command::cargo_bin("zad").expect("zad binary built");
c.env("ZAD_SECRETS_MEMORY", "1");
c
}
fn seed_global_creds(home: &std::path::Path) {
let p = home
.join(".zad")
.join("services")
.join("discord")
.join("config.toml");
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(
&p,
"application_id = \"1234567890\"\n\
scopes = [\"guilds\", \"messages.read\", \"messages.send\"]\n\
default_guild = \"42\"\n",
)
.unwrap();
}
fn enable_discord(home: &std::path::Path, project: &std::path::Path) {
bin()
.env("ZAD_HOME_OVERRIDE", home)
.current_dir(project)
.args(["service", "enable", "discord"])
.assert()
.success();
}
fn write_unsigned_local_permissions(home: &std::path::Path, project: &std::path::Path, body: &str) {
let slug = common::project_slug(project);
let p = home
.join(".zad")
.join("projects")
.join(&slug)
.join("services")
.join("discord")
.join("permissions.toml");
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(&p, body).unwrap();
}
fn write_signed_local_permissions(
home: &std::path::Path,
project: &std::path::Path,
body: &str,
) -> std::path::PathBuf {
use zad::permissions::SigningKey;
use zad::permissions::signing::SIGNING_ACCOUNT;
use zad::service::discord::permissions::{self as perms, DiscordPermissionsRaw};
let slug = common::project_slug(project);
let p = home
.join(".zad")
.join("projects")
.join(&slug)
.join("services")
.join("discord")
.join("permissions.toml");
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
zad::secrets::use_memory_backend();
unsafe {
std::env::set_var("ZAD_HOME_OVERRIDE", home);
}
let key = SigningKey::generate();
let _ = zad::secrets::delete(SIGNING_ACCOUNT);
zad::secrets::store(SIGNING_ACCOUNT, &key.to_keychain_encoded()).unwrap();
let raw: DiscordPermissionsRaw =
toml::from_str(body).expect("permissions body must be valid TOML");
perms::save_file(&p, &raw, &key).unwrap();
p
}
#[test]
#[serial]
fn send_with_unsigned_permissions_echoes_json_and_exits_3() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global_creds(home.path());
enable_discord(home.path(), project.path());
write_unsigned_local_permissions(home.path(), project.path(), "[send]\n");
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["discord", "send", "--channel", "12345", "--json", "hello"])
.assert()
.code(3)
.stdout(contains("\"echoed\""))
.stdout(contains("\"kind\": \"not_trusted\""))
.stdout(contains("\"target_id\": \"12345\""))
.stdout(contains("\"command\": \"discord.send\""));
}
#[test]
#[serial]
fn send_with_unsigned_permissions_echoes_human_summary() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global_creds(home.path());
enable_discord(home.path(), project.path());
write_unsigned_local_permissions(home.path(), project.path(), "[send]\n");
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["discord", "send", "--channel", "12345", "hello"])
.assert()
.code(3)
.stdout(contains("would have run:"))
.stdout(contains("would send 5 chars to channel 12345"))
.stdout(contains("reason:"))
.stdout(contains("not trusted"));
}
#[test]
#[serial]
fn send_with_tampered_permissions_echoes_signature_invalid() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global_creds(home.path());
enable_discord(home.path(), project.path());
let p = write_signed_local_permissions(
home.path(),
project.path(),
"[send.channels]\nallow = [\"bot-*\"]\n",
);
let body = std::fs::read_to_string(&p).unwrap();
let tampered = body.replace("bot-*", "bot-?");
assert_ne!(body, tampered, "sentinel substitution must have matched");
std::fs::write(&p, &tampered).unwrap();
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["discord", "send", "--channel", "12345", "--json", "hi"])
.assert()
.code(3)
.stdout(contains("\"kind\": \"signature_invalid\""))
.stdout(contains("\"target_id\": \"12345\""));
}
#[test]
#[serial]
fn permissions_show_with_unsigned_file_hard_fails_not_echoes() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global_creds(home.path());
enable_discord(home.path(), project.path());
write_unsigned_local_permissions(home.path(), project.path(), "[send]\n");
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["discord", "permissions", "show"])
.assert()
.failure()
.code(1)
.stderr(contains("not trusted"));
}
#[test]
#[serial]
fn dry_run_without_permissions_does_not_echo() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
seed_global_creds(home.path());
enable_discord(home.path(), project.path());
bin()
.env("ZAD_HOME_OVERRIDE", home.path())
.current_dir(project.path())
.args(["discord", "send", "--channel", "12345", "--dry-run", "hi"])
.assert()
.success()
.stdout(contains("\"discord.send\""))
.stdout(contains("\"echoed\"").not())
.stdout(contains("\"kind\": \"not_trusted\"").not());
}