use std::fs;
use std::path::Path;
use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::TempDir;
fn quorum() -> Command {
Command::cargo_bin("quorum").expect("quorum binary built")
}
fn init_repo() -> TempDir {
let td = TempDir::new().unwrap();
git2::Repository::init(td.path()).unwrap();
td
}
fn quorum_in(repo: &Path, cfg_dir: &Path) -> Command {
let mut c = quorum();
c.current_dir(repo)
.env("APPDATA", cfg_dir) .env("XDG_CONFIG_HOME", cfg_dir) .env_remove("QUORUM_LIPPA_SESSION")
.env_remove("QUORUM_LIPPA_EMAIL")
.env_remove("QUORUM_LIPPA_PASSWORD");
c
}
#[test]
fn login_non_interactive_missing_email_exits_2() {
let td = init_repo();
let cfg = TempDir::new().unwrap();
let url = "http://127.0.0.1:1".to_string(); quorum_in(td.path(), cfg.path())
.args([
"auth",
"login",
"--non-interactive",
"--no-keyring",
"--url",
])
.arg(&url)
.env("QUORUM_LIPPA_PASSWORD", "ignored")
.assert()
.failure()
.code(predicate::eq(2))
.stderr(predicate::str::contains(
"missing required env var: QUORUM_LIPPA_EMAIL",
));
}
#[test]
fn login_non_interactive_missing_password_exits_2() {
let td = init_repo();
let cfg = TempDir::new().unwrap();
let url = "http://127.0.0.1:1".to_string();
quorum_in(td.path(), cfg.path())
.args([
"auth",
"login",
"--non-interactive",
"--no-keyring",
"--url",
])
.arg(&url)
.env("QUORUM_LIPPA_EMAIL", "x@example.com")
.assert()
.failure()
.code(predicate::eq(2))
.stderr(predicate::str::contains(
"missing required env var: QUORUM_LIPPA_PASSWORD",
));
}
#[test]
fn login_non_interactive_empty_password_treated_as_missing() {
let td = init_repo();
let cfg = TempDir::new().unwrap();
let url = "http://127.0.0.1:1".to_string();
quorum_in(td.path(), cfg.path())
.args([
"auth",
"login",
"--non-interactive",
"--no-keyring",
"--url",
])
.arg(&url)
.env("QUORUM_LIPPA_EMAIL", "x@example.com")
.env("QUORUM_LIPPA_PASSWORD", "")
.assert()
.failure()
.code(predicate::eq(2));
}
#[test]
fn login_non_interactive_happy_path_writes_session_file() {
let mut server = mockito::Server::new();
let mock = server
.mock("POST", "/api/v1/auth/login")
.with_status(200)
.with_header("content-type", "application/json")
.with_header(
"set-cookie",
"session=preflight-token-xyz; Path=/; HttpOnly",
)
.with_body(r#"{"ok":true}"#)
.create();
let td = init_repo();
let cfg = TempDir::new().unwrap();
quorum_in(td.path(), cfg.path())
.args([
"auth",
"login",
"--non-interactive",
"--no-keyring",
"--url",
])
.arg(server.url())
.env("QUORUM_LIPPA_EMAIL", "alice@example.com")
.env("QUORUM_LIPPA_PASSWORD", "hunter2-very-secret")
.assert()
.success();
mock.assert();
let host = url::Url::parse(&server.url())
.unwrap()
.host_str()
.unwrap()
.to_string();
let port = url::Url::parse(&server.url()).unwrap().port().unwrap();
let file_root = cfg.path().join("quorum").join("sessions");
let files: Vec<_> = fs::read_dir(&file_root).unwrap().collect();
assert!(
!files.is_empty(),
"session file written under {:?}",
file_root
);
let body = fs::read_to_string(files[0].as_ref().unwrap().path()).unwrap();
assert!(
body.contains("preflight-token-xyz"),
"session file should hold the cookie value; got {body:?}"
);
assert!(!body.contains("hunter2"));
let _ = host;
let _ = port;
}
#[test]
fn show_session_non_tty_without_y_exits_2() {
let td = init_repo();
let cfg = TempDir::new().unwrap();
let mut server = mockito::Server::new();
server
.mock("POST", "/api/v1/auth/login")
.with_status(200)
.with_header("set-cookie", "session=visible-cookie-value; Path=/")
.with_body("{}")
.create();
quorum_in(td.path(), cfg.path())
.args([
"auth",
"login",
"--non-interactive",
"--no-keyring",
"--url",
])
.arg(server.url())
.env("QUORUM_LIPPA_EMAIL", "x@example.com")
.env("QUORUM_LIPPA_PASSWORD", "p")
.assert()
.success();
quorum_in(td.path(), cfg.path())
.args(["auth", "status", "--show-session", "--no-keyring", "--url"])
.arg(server.url())
.assert()
.failure()
.code(predicate::eq(2))
.stderr(predicate::str::contains("non-TTY"));
}
#[test]
fn show_session_with_y_prints_cookie_value_to_stdout() {
let td = init_repo();
let cfg = TempDir::new().unwrap();
let mut server = mockito::Server::new();
server
.mock("POST", "/api/v1/auth/login")
.with_status(200)
.with_header("set-cookie", "session=visible-cookie-value-123; Path=/")
.with_body("{}")
.create();
quorum_in(td.path(), cfg.path())
.args([
"auth",
"login",
"--non-interactive",
"--no-keyring",
"--url",
])
.arg(server.url())
.env("QUORUM_LIPPA_EMAIL", "x@example.com")
.env("QUORUM_LIPPA_PASSWORD", "p")
.assert()
.success();
quorum_in(td.path(), cfg.path())
.args([
"auth",
"status",
"--show-session",
"-y",
"--no-keyring",
"--url",
])
.arg(server.url())
.assert()
.success()
.stdout(predicate::str::contains("visible-cookie-value-123"));
}
#[test]
fn show_session_no_auth_stored_exits_2() {
let td = init_repo();
let cfg = TempDir::new().unwrap();
let url = "http://127.0.0.1:1".to_string();
quorum_in(td.path(), cfg.path())
.args([
"auth",
"status",
"--show-session",
"-y",
"--no-keyring",
"--url",
])
.arg(&url)
.assert()
.failure()
.code(predicate::eq(2))
.stderr(predicate::str::contains("no session stored"));
}
#[test]
fn session_env_var_precedence_note_suppressed_under_hook_mode() {
let td = init_repo();
let cfg = TempDir::new().unwrap();
let mut server = mockito::Server::new();
server
.mock("POST", "/api/v1/auth/login")
.with_status(200)
.with_header("set-cookie", "session=keyring-cookie; Path=/")
.with_body("{}")
.create();
quorum_in(td.path(), cfg.path())
.args([
"auth",
"login",
"--non-interactive",
"--no-keyring",
"--url",
])
.arg(server.url())
.env("QUORUM_LIPPA_EMAIL", "x@example.com")
.env("QUORUM_LIPPA_PASSWORD", "p")
.assert()
.success();
quorum_in(td.path(), cfg.path())
.args(["link", "--project", "p_x", "--url"])
.arg(server.url())
.assert()
.success();
server
.mock("POST", "/api/v1/consensus/sessions")
.with_status(401)
.create();
quorum_in(td.path(), cfg.path())
.args(["review", "--hook-mode=pre-commit", "--no-keyring"])
.env("QUORUM_LIPPA_SESSION", "env-cookie-value")
.assert()
.stderr(predicate::str::contains("env var takes precedence").not());
}
#[test]
fn session_env_var_precedence_note_emitted_in_interactive_review() {
let td = init_repo();
let cfg = TempDir::new().unwrap();
let mut server = mockito::Server::new();
server
.mock("POST", "/api/v1/auth/login")
.with_status(200)
.with_header("set-cookie", "session=keyring-cookie; Path=/")
.with_body("{}")
.create();
quorum_in(td.path(), cfg.path())
.args([
"auth",
"login",
"--non-interactive",
"--no-keyring",
"--url",
])
.arg(server.url())
.env("QUORUM_LIPPA_EMAIL", "x@example.com")
.env("QUORUM_LIPPA_PASSWORD", "p")
.assert()
.success();
quorum_in(td.path(), cfg.path())
.args(["link", "--project", "p_x", "--url"])
.arg(server.url())
.assert()
.success();
server
.mock("POST", "/api/v1/consensus/sessions")
.with_status(401)
.create();
quorum_in(td.path(), cfg.path())
.args(["review", "--no-keyring"])
.env("QUORUM_LIPPA_SESSION", "env-cookie-value")
.assert()
.stderr(predicate::str::contains("env var takes precedence"));
}
#[test]
fn tracing_at_trace_level_does_not_leak_session_env() {
let td = init_repo();
let cfg = TempDir::new().unwrap();
let mut server = mockito::Server::new();
server
.mock("POST", "/api/v1/auth/login")
.with_status(200)
.with_header("set-cookie", "session=TRACE-LEAK-CANARY-XYZ; Path=/")
.with_body("{}")
.create();
quorum_in(td.path(), cfg.path())
.args([
"auth",
"login",
"--non-interactive",
"--no-keyring",
"--url",
])
.arg(server.url())
.env("QUORUM_LIPPA_EMAIL", "x@example.com")
.env("QUORUM_LIPPA_PASSWORD", "PASSWORD-LEAK-CANARY-XYZ")
.env("RUST_LOG", "trace")
.assert()
.success()
.stderr(predicate::str::contains("TRACE-LEAK-CANARY-XYZ").not())
.stderr(predicate::str::contains("PASSWORD-LEAK-CANARY-XYZ").not());
}