use aethershell::env::Env;
use aethershell::value::Value;
use std::sync::Mutex;
static ENV: Mutex<()> = Mutex::new(());
fn call(name: &str, args: &[&str]) -> Result<Value, String> {
let mut env = Env::new();
let args = args.iter().map(|a| Value::Str(a.to_string())).collect();
aethershell::builtins::call(name, args, &mut env).map_err(|e| e.to_string())
}
fn human() {
std::env::set_var("AETHER_MODE", "human");
std::env::remove_var("AETHER_POLICY");
aethershell::safety::set_principal(None);
}
fn field(v: &Value, key: &str) -> String {
match v {
Value::Record(r) => match r.get(key) {
Some(Value::Str(s)) => s.clone(),
other => panic!("field {key} is {other:?}, not a string"),
},
other => panic!("expected a record, got {other:?}"),
}
}
#[test]
fn a_registered_user_can_log_in_and_becomes_the_principal() {
let _l = ENV.lock().unwrap_or_else(|e| e.into_inner());
human();
let reg = call("rbac_register", &["ada", "correct horse battery staple"])
.expect("registration should succeed");
let uid = field(®, "user_id");
assert_eq!(
aethershell::safety::current_principal(),
None,
"registering is not logging in"
);
let session = call("rbac_login", &["ada", "correct horse battery staple"])
.expect("login with the right password should succeed");
assert_eq!(field(&session, "user"), "ada");
assert_eq!(field(&session, "user_id"), uid);
assert!(!field(&session, "session").is_empty());
assert_eq!(
aethershell::safety::current_principal(),
Some(uid),
"a successful login must set the acting principal"
);
call("rbac_logout", &[]).unwrap();
}
#[test]
fn a_wrong_password_is_refused_and_leaves_the_caller_anonymous() {
let _l = ENV.lock().unwrap_or_else(|e| e.into_inner());
human();
call("rbac_register", &["grace", "hopper-1906"]).expect("registration should succeed");
aethershell::safety::set_principal(None);
let err = call("rbac_login", &["grace", "hopper-1907"])
.expect_err("the wrong password must not authenticate");
assert!(
err.contains("invalid credentials"),
"expected a credential refusal, got: {err}"
);
assert!(
!err.contains("password") || !err.to_lowercase().contains("user not found"),
"the refusal names which half was wrong: {err}"
);
assert_eq!(
aethershell::safety::current_principal(),
None,
"a failed login must not set a principal"
);
let unknown = call("rbac_login", &["nobody_at_all", "hopper-1906"])
.expect_err("an unknown user must not authenticate");
assert_eq!(unknown, err, "the two refusals must be indistinguishable");
}
#[test]
fn logging_out_drops_the_principal_and_the_session() {
let _l = ENV.lock().unwrap_or_else(|e| e.into_inner());
human();
call("rbac_register", &["linus", "torvalds-1991"]).unwrap();
call("rbac_login", &["linus", "torvalds-1991"]).unwrap();
assert!(matches!(
call("rbac_session", &[]).unwrap(),
Value::Record(_)
));
assert_eq!(call("rbac_logout", &[]).unwrap(), Value::Bool(true));
assert_eq!(aethershell::safety::current_principal(), None);
assert_eq!(call("rbac_session", &[]).unwrap(), Value::Null);
assert_eq!(call("rbac_logout", &[]).unwrap(), Value::Bool(false));
}
#[test]
fn passwords_are_salted_so_equal_passwords_do_not_collide() {
use aethershell::auth::{hash_password, verify_password};
let a = hash_password("same password").unwrap();
let b = hash_password("same password").unwrap();
assert_ne!(a, b, "two hashes of one password are identical -- unsalted");
assert!(a.starts_with("$argon2"), "not a PHC argon2 string: {a}");
assert!(verify_password("same password", &a));
assert!(verify_password("same password", &b));
assert!(!verify_password("different password", &a));
assert!(!verify_password("anything", "not-a-hash"));
assert!(!verify_password("anything", ""));
}
#[test]
fn an_agent_cannot_log_itself_in() {
let _l = ENV.lock().unwrap_or_else(|e| e.into_inner());
human();
call("rbac_register", &["agent_target", "s3cret-passphrase"]).unwrap();
aethershell::safety::set_principal(None);
std::env::set_var("AETHER_MODE", "agent");
for (name, args) in [
("rbac_login", &["agent_target", "s3cret-passphrase"][..]),
("rbac_register", &["another", "s3cret-passphrase"][..]),
] {
match call(name, args) {
Err(e) => assert!(
e.contains("E_POLICY_DENY"),
"{name} failed for the wrong reason: {e}"
),
Ok(v) => panic!("{name} was allowed in agent mode, returning {v:?}"),
}
}
assert_eq!(
aethershell::safety::current_principal(),
None,
"the denied login must not have set a principal"
);
std::env::set_var("AETHER_MODE", "human");
}
#[test]
fn a_password_is_never_read_from_a_pipe() {
let _l = ENV.lock().unwrap_or_else(|e| e.into_inner());
human();
let err = call("rbac_login", &["someone"])
.expect_err("a passwordless login must not silently read stdin");
assert!(
err.contains("not a terminal"),
"expected a terminal refusal, got: {err}"
);
}
#[test]
fn an_unknown_username_costs_the_same_as_a_wrong_password() {
let _l = ENV.lock().unwrap_or_else(|e| e.into_inner());
human();
call("rbac_register", &["timed", "a-real-password"]).unwrap();
aethershell::safety::set_principal(None);
let bench = |user: &str| {
let t = std::time::Instant::now();
for _ in 0..3 {
let _ = call("rbac_login", &[user, "not-the-password"]);
}
t.elapsed()
};
let _ = call("rbac_login", &["no_such_user_warmup", "x"]);
let known = bench("timed");
let unknown = bench("definitely_no_such_user");
let ratio = unknown.as_secs_f64() / known.as_secs_f64().max(f64::MIN_POSITIVE);
assert!(
ratio > 0.4,
"an unknown username answers {ratio:.2}x as fast as a wrong password ({unknown:?} vs {known:?}) -- that gap is a username oracle"
);
}