//! `pic api-keys` — mint / ls / rm journeys. //! //! Server semantics asserted here: //! * `mint` emits the `raw_token` *exactly once* and never on `ls`. //! * A minted key is a valid bearer for `/auth/me`. //! * After `rm`, the same token is rejected (401). use predicates::prelude::*; use serde_json::Value; use crate::common; #[ignore = "needs DATABASE_URL pointing at a running Postgres"] #[test] fn mint_prints_raw_token_once_and_ls_omits_it() { let Some(fx) = common::fixture_or_skip() else { return; }; let env = common::admin_env(fx); let name = format!("pic-cli-mint-{}", common::unique_slug("k")); let out = common::pic_as(&env) .args([ "--output", "json", "api-keys", "mint", &name, "--scope", "script:read", ]) .output() .expect("api-keys mint"); assert!(out.status.success(), "mint failed: {out:?}"); let body: Value = serde_json::from_slice(&out.stdout).expect("JSON"); let token = body["token"] .as_str() .expect("mint should expose `token`") .to_string(); let key_id = body["id"] .as_str() .expect("mint should expose `id`") .to_string(); assert!( token.starts_with("pic_"), "tokens are pic_-prefixed: {token}" ); // `ls` must NEVER carry the raw token. The key row should appear, // identified by name, but `token` is mint-only. let ls = common::pic_as(&env) .args(["--output", "json", "api-keys", "ls"]) .output() .expect("api-keys ls"); assert!(ls.status.success(), "ls failed: {ls:?}"); let ls_body: Value = serde_json::from_slice(&ls.stdout).expect("JSON"); let arr = ls_body.as_array().expect("array"); let row = arr .iter() .find(|r| r.get("id").and_then(Value::as_str) == Some(key_id.as_str())) .expect("our key in ls"); assert!( row.get("token").is_none(), "ls must not expose raw_token: {row}" ); // Cleanup so we don't leak keys across runs. common::pic_as(&env) .args(["api-keys", "rm", &key_id]) .assert() .success(); } #[ignore = "needs DATABASE_URL pointing at a running Postgres"] #[test] fn minted_key_works_as_bearer() { let Some(fx) = common::fixture_or_skip() else { return; }; let env = common::admin_env(fx); let name = format!("pic-cli-bearer-{}", common::unique_slug("k")); let mint = common::pic_as(&env) .args([ "--output", "json", "api-keys", "mint", &name, "--scope", "script:read", ]) .output() .expect("mint"); assert!(mint.status.success()); let body: Value = serde_json::from_slice(&mint.stdout).unwrap(); let token = body["token"].as_str().unwrap().to_string(); let id = body["id"].as_str().unwrap().to_string(); // Drive whoami with the minted token — proves the bearer string we // captured really is what the server stamped. let key_env = common::custom_env(&fx.url, &token); common::seed_credentials(&key_env, &fx.admin_username); common::pic_as(&key_env) .args(["whoami"]) .assert() .success() .stdout(predicate::str::contains(fx.admin_username.as_str())); common::pic_as(&env) .args(["api-keys", "rm", &id]) .assert() .success(); } /// After `rm`, the bearer token is dead server-side: a follow-up /// `whoami` driven by it must 401, not 500. #[ignore = "needs DATABASE_URL pointing at a running Postgres"] #[test] fn rm_revokes_the_token() { let Some(fx) = common::fixture_or_skip() else { return; }; let env = common::admin_env(fx); let name = format!("pic-cli-rm-{}", common::unique_slug("k")); let mint = common::pic_as(&env) .args([ "--output", "json", "api-keys", "mint", &name, "--scope", "script:read", ]) .output() .expect("mint"); let body: Value = serde_json::from_slice(&mint.stdout).unwrap(); let token = body["token"].as_str().unwrap().to_string(); let id = body["id"].as_str().unwrap().to_string(); common::pic_as(&env) .args(["api-keys", "rm", &id]) .assert() .success() .stdout(predicate::str::contains(format!("Revoked api-key {id}"))); let dead = common::custom_env(&fx.url, &token); common::pic_as(&dead) .args(["whoami"]) .assert() .failure() .stderr(predicate::str::contains("HTTP 401")); } #[ignore = "needs DATABASE_URL pointing at a running Postgres"] #[test] fn mint_with_unknown_scope_is_rejected_client_side() { let Some(fx) = common::fixture_or_skip() else { return; }; let env = common::admin_env(fx); common::pic_as(&env) .args(["api-keys", "mint", "doomed", "--scope", "script:nope"]) .assert() .failure() .stderr(predicate::str::contains("unknown scope")); }