use std::{fs, process::Command};
use serde_json::{Value, json};
use tempfile::tempdir;
const SECRET: &str = "test-hmac-secret";
fn bin() -> Command {
Command::new(env!("CARGO_BIN_EXE_challenge"))
}
fn run_ok(args: &[&str]) -> String {
let output = bin()
.args(args)
.output()
.unwrap_or_else(|error| panic!("failed to run challenge {args:?}: {error}"));
assert!(
output.status.success(),
"challenge {args:?} failed\nstatus: {}\nstdout:\n{}\nstderr:\n{}",
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).expect("stdout should be UTF-8")
}
fn run_fail(args: &[&str]) -> String {
let output = bin()
.args(args)
.output()
.unwrap_or_else(|error| panic!("failed to run challenge {args:?}: {error}"));
assert!(
!output.status.success(),
"challenge {args:?} unexpectedly succeeded\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stderr).expect("stderr should be UTF-8")
}
#[test]
fn help_prints_usage() {
let stdout = run_ok(&["--help"]);
assert!(stdout.contains("A lightweight CLI for ALTCHA"));
assert!(stdout.contains("create"));
assert!(stdout.contains("verify-payload"));
}
#[test]
fn create_requires_signed_challenges_by_default() {
let stderr = run_fail(&["create", "--cost", "1"]);
assert!(stderr.contains("--hmac-secret is required"));
}
#[test]
fn create_rejects_conflicting_counter_options() {
let stderr = run_fail(&[
"create",
"--cost",
"1",
"--counter",
"1",
"--random-counter",
"--hmac-secret",
SECRET,
"--hmac-key-secret",
"test-key-secret",
]);
assert!(stderr.contains("use either --counter or --random-counter"));
}
#[test]
fn create_solve_verify_and_verify_payload_roundtrip() {
let temp = tempdir().unwrap();
let challenge_path = temp.path().join("challenge.json");
let solution_path = temp.path().join("solution.json");
let register_path = temp.path().join("register-request.json");
let challenge_json = run_ok(&[
"--compact",
"create",
"--cost",
"1",
"--key-prefix",
"00",
"--expires-in",
"600",
"--hmac-secret",
SECRET,
"--data",
"action=register",
]);
fs::write(&challenge_path, &challenge_json).unwrap();
let challenge: Value = serde_json::from_str(&challenge_json).unwrap();
assert!(challenge.get("signature").is_some());
let solution_json = run_ok(&[
"--compact",
"solve",
"--challenge",
challenge_path.to_str().unwrap(),
"--timeout-ms",
"30000",
]);
fs::write(&solution_path, &solution_json).unwrap();
let solution: Value = serde_json::from_str(&solution_json).unwrap();
assert!(solution.get("counter").is_some());
assert!(solution.get("derivedKey").is_some());
let verify_json = run_ok(&[
"--compact",
"verify",
"--challenge",
challenge_path.to_str().unwrap(),
"--solution",
solution_path.to_str().unwrap(),
"--secret",
SECRET,
"--fail-on-invalid",
]);
let verify: Value = serde_json::from_str(&verify_json).unwrap();
assert_eq!(verify.get("verified"), Some(&json!(true)));
assert_eq!(verify.get("expired"), Some(&json!(false)));
let register_request = json!({
"username": "alice",
"password": "not-a-real-password",
"altcha": {
"challenge": challenge,
"solution": solution,
}
});
fs::write(
®ister_path,
serde_json::to_string_pretty(®ister_request).unwrap(),
)
.unwrap();
let verify_payload_json = run_ok(&[
"--compact",
"verify-payload",
"--payload",
register_path.to_str().unwrap(),
"--secret",
SECRET,
"--fail-on-invalid",
]);
let verify_payload: Value = serde_json::from_str(&verify_payload_json).unwrap();
assert_eq!(verify_payload.get("verified"), Some(&json!(true)));
}