use crate::support::*;
use std::fs;
#[test]
fn test_knock_creates_request_file() {
let t = Test::init("alice");
let setup_output = t.cmd().arg("setup").output().unwrap();
assert_success(&setup_output);
let output = t.cmd().args(["knock", "bob"]).output().unwrap();
assert_success(&output);
assert_stdout_contains(&output, "created access request");
let request_path = t.dir.path().join(".dugout/requests/default/bob.pub");
assert!(request_path.exists(), "request file should exist");
let pubkey = fs::read_to_string(&request_path).unwrap();
assert!(pubkey.trim().starts_with("age1"));
}
#[test]
fn test_knock_without_global_identity_fails() {
let t = Test::init("alice");
let output = t.cmd().args(["knock", "bob"]).output().unwrap();
assert_failure(&output);
assert_stderr_contains(&output, "no identity found");
assert_stdout_contains(&output, "dugout setup");
}
#[test]
fn test_knock_when_already_member() {
let t = Test::new();
let setup_output = t.cmd().arg("setup").output().unwrap();
assert_success(&setup_output);
let pubkey_path = t.home.path().join(".dugout/identity.pub");
let global_pubkey = fs::read_to_string(&pubkey_path).unwrap().trim().to_string();
let init_output = t
.cmd()
.args(["init", "--no-banner", "--name", "alice"])
.output()
.unwrap();
assert_success(&init_output);
let config_path = t.dir.path().join(".dugout.toml");
let config_content = fs::read_to_string(&config_path).unwrap();
let updated_config = config_content
.lines()
.map(|line| {
if line.contains("age1") && !line.contains(&global_pubkey) {
format!("alice = \"{}\"", global_pubkey)
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n");
fs::write(&config_path, updated_config).unwrap();
let output = t.cmd().args(["knock", "alice"]).output().unwrap();
assert_success(&output);
assert_stdout_contains(&output, "already have access");
}
#[test]
fn test_pending_lists_requests() {
let t = Test::init("alice");
let setup_output = t.cmd().arg("setup").output().unwrap();
assert_success(&setup_output);
let knock_output = t.cmd().args(["knock", "bob"]).output().unwrap();
assert_success(&knock_output);
let output = t.cmd().arg("pending").output().unwrap();
assert_success(&output);
assert_stdout_contains(&output, "bob");
assert_stdout_contains(&output, "age1");
}
#[test]
fn test_pending_when_no_requests() {
let t = Test::init("alice");
let output = t.cmd().arg("pending").output().unwrap();
assert_success(&output);
assert_stdout_contains(&output, "no pending requests");
}
#[test]
fn test_admit_approves_request() {
let t = Test::init("alice");
let setup_output = t.cmd().arg("setup").output().unwrap();
assert_success(&setup_output);
let knock_output = t.cmd().args(["knock", "bob"]).output().unwrap();
assert_success(&knock_output);
let output = t.cmd().args(["admit", "bob"]).output().unwrap();
assert_success(&output);
assert_stdout_contains(&output, "admitted");
let request_path = t.dir.path().join(".dugout/requests/default/bob.pub");
assert!(!request_path.exists(), "request file should be deleted");
let team_output = t.team_list();
assert_success(&team_output);
assert_stdout_contains(&team_output, "bob");
}
#[test]
fn test_admit_nonexistent_request_fails() {
let t = Test::init("alice");
let output = t.cmd().args(["admit", "nonexistent"]).output().unwrap();
assert_failure(&output);
assert_stderr_contains(&output, "no pending request");
}
#[test]
fn test_knock_pending_admit_workflow() {
let t = Test::init("alice");
let setup_output = t.cmd().arg("setup").output().unwrap();
assert_success(&setup_output);
let knock_output = t.cmd().args(["knock", "bob"]).output().unwrap();
assert_success(&knock_output);
let pending_output = t.cmd().arg("pending").output().unwrap();
assert_success(&pending_output);
assert_stdout_contains(&pending_output, "bob");
let admit_output = t.cmd().args(["admit", "bob"]).output().unwrap();
assert_success(&admit_output);
let pending_output2 = t.cmd().arg("pending").output().unwrap();
assert_success(&pending_output2);
assert_stdout_contains(&pending_output2, "no pending requests");
let team_output = t.team_list();
assert_success(&team_output);
assert_stdout_contains(&team_output, "bob");
}
#[test]
fn test_full_onboarding_with_separate_identities() {
let t = Test::new();
let alice_home = tempfile::TempDir::new().unwrap();
let output = t
.cmd()
.env("HOME", alice_home.path())
.env("USERPROFILE", alice_home.path())
.arg("setup")
.output()
.unwrap();
assert_success(&output);
let alice_pubkey = fs::read_to_string(alice_home.path().join(".dugout/identity.pub"))
.unwrap()
.trim()
.to_string();
let output = t
.cmd()
.env("HOME", alice_home.path())
.env("USERPROFILE", alice_home.path())
.args(["init", "--name", "alice"])
.output()
.unwrap();
assert_success(&output);
let output = t
.cmd()
.env("HOME", alice_home.path())
.env("USERPROFILE", alice_home.path())
.args(["set", "API_KEY", "super_secret_123"])
.output()
.unwrap();
assert_success(&output);
let output = t
.cmd()
.env("HOME", alice_home.path())
.env("USERPROFILE", alice_home.path())
.args(["get", "API_KEY"])
.output()
.unwrap();
assert_success(&output);
assert_eq!(stdout(&output).trim(), "super_secret_123");
let bob_home = tempfile::TempDir::new().unwrap();
let output = t
.cmd()
.env("HOME", bob_home.path())
.env("USERPROFILE", bob_home.path())
.arg("setup")
.output()
.unwrap();
assert_success(&output);
let bob_pubkey = fs::read_to_string(bob_home.path().join(".dugout/identity.pub"))
.unwrap()
.trim()
.to_string();
assert_ne!(
alice_pubkey, bob_pubkey,
"alice and bob should have different keys"
);
let output = t
.cmd()
.env("HOME", bob_home.path())
.env("USERPROFILE", bob_home.path())
.args(["knock", "bob"])
.output()
.unwrap();
assert_success(&output);
let request_path = t.dir.path().join(".dugout/requests/default/bob.pub");
assert!(request_path.exists());
let request_key = fs::read_to_string(&request_path).unwrap();
assert_eq!(request_key.trim(), bob_pubkey);
let output = t
.cmd()
.env("HOME", alice_home.path())
.env("USERPROFILE", alice_home.path())
.args(["admit", "bob"])
.output()
.unwrap();
assert_success(&output);
assert!(!request_path.exists());
let output = t
.cmd()
.env("HOME", alice_home.path())
.env("USERPROFILE", alice_home.path())
.args(["team", "list", "--json"])
.output()
.unwrap();
assert_success(&output);
let team_json = stdout(&output);
assert!(
team_json.contains(&alice_pubkey),
"team should contain alice's key"
);
assert!(
team_json.contains(&bob_pubkey),
"team should contain bob's key"
);
let output = t
.cmd()
.env("HOME", bob_home.path())
.env("USERPROFILE", bob_home.path())
.args(["get", "API_KEY"])
.output()
.unwrap();
assert_success(&output);
assert_eq!(stdout(&output).trim(), "super_secret_123");
}
#[test]
fn test_knock_uses_global_identity_key() {
let t = Test::init("alice");
let setup_output = t.cmd().arg("setup").output().unwrap();
assert_success(&setup_output);
let global_pubkey = fs::read_to_string(t.home.path().join(".dugout/identity.pub"))
.unwrap()
.trim()
.to_string();
let output = t.cmd().args(["knock", "bob"]).output().unwrap();
assert_success(&output);
let request_key = fs::read_to_string(t.dir.path().join(".dugout/requests/default/bob.pub"))
.unwrap()
.trim()
.to_string();
assert_eq!(
request_key, global_pubkey,
"knock should use the global identity key"
);
}
#[test]
fn test_knock_output_includes_instructions() {
let t = Test::init("alice");
let setup_output = t.cmd().arg("setup").output().unwrap();
assert_success(&setup_output);
let output = t.cmd().args(["knock", "bob"]).output().unwrap();
assert_success(&output);
assert_stdout_contains(&output, "created access request");
assert_stdout_contains(&output, ".dugout/requests/default/bob.pub");
}
#[test]
fn test_knock_rejects_invalid_member_name() {
let t = Test::init("alice");
let setup_output = t.cmd().arg("setup").output().unwrap();
assert_success(&setup_output);
let output = t.cmd().args(["knock", "../bob"]).output().unwrap();
assert_failure(&output);
assert_stderr_contains(&output, "invalid member name");
let request_path = t.dir.path().join(".dugout/requests/default/../bob.pub");
assert!(
!request_path.exists(),
"invalid names must not create request files"
);
}
#[test]
fn test_knock_idempotent_same_key() {
let t = Test::init("alice");
let setup_output = t.cmd().arg("setup").output().unwrap();
assert_success(&setup_output);
let output1 = t.cmd().args(["knock", "bob"]).output().unwrap();
assert_success(&output1);
assert_stdout_contains(&output1, "created access request");
let output2 = t.cmd().args(["knock", "bob"]).output().unwrap();
assert_success(&output2);
assert_stdout_contains(&output2, "already exists with your key");
}
#[test]
fn test_knock_different_key_fails() {
let t = Test::init("alice");
let home1 = tempfile::TempDir::new().unwrap();
let output = t
.cmd()
.env("HOME", home1.path())
.env("USERPROFILE", home1.path())
.arg("setup")
.output()
.unwrap();
assert_success(&output);
let output = t
.cmd()
.env("HOME", home1.path())
.env("USERPROFILE", home1.path())
.args(["knock", "bob"])
.output()
.unwrap();
assert_success(&output);
let home2 = tempfile::TempDir::new().unwrap();
let output = t
.cmd()
.env("HOME", home2.path())
.env("USERPROFILE", home2.path())
.arg("setup")
.output()
.unwrap();
assert_success(&output);
let output = t
.cmd()
.env("HOME", home2.path())
.env("USERPROFILE", home2.path())
.args(["knock", "bob"])
.output()
.unwrap();
assert_failure(&output);
assert_stderr_contains(&output, "already exists with a different key");
}
#[test]
fn test_admit_corrupted_request_file() {
let t = Test::init("alice");
let request_dir = t.dir.path().join(".dugout/requests/default");
fs::create_dir_all(&request_dir).unwrap();
fs::write(request_dir.join("baduser.pub"), "not-a-valid-age-key\n").unwrap();
let output = t.cmd().args(["admit", "baduser"]).output().unwrap();
assert_failure(&output);
assert_stderr_contains(&output, "invalid");
}