Files
wfe/wfectl/tests/auth_commands.rs

195 lines
6.1 KiB
Rust
Raw Normal View History

feat(wfectl): new CLI client + wfe-ci builder image wfectl is a command-line client for wfe-server with 17 subcommands covering the full workflow lifecycle: * Auth: login (OAuth2 PKCE via Ory Hydra), logout, whoami * Definitions: register (YAML → gRPC), validate (local compile), definitions list * Instances: run, get, list, cancel, suspend, resume * Events: publish * Streaming: watch (lifecycle), logs, search-logs (full-text) Key design points: * `validate` compiles YAML locally via `wfe-yaml::load_workflow_from_str` with the full executor feature set enabled — instant feedback, no server round-trip, no auth required. Uses the same compile path as the server's `register` RPC so what passes validation is guaranteed to register. * Lookup commands accept either UUID or human name; the server resolves the identifier for us. Display tables show both columns. * `run --name <N>` lets users override the auto-generated `{def_id}-{N}` instance name when they want a sticky reference. * Table and JSON output formats, shared bearer-token or cached-login auth path, direct token injection via `WFECTL_TOKEN`. * 5 new unit tests for the validate command cover happy path, unknown step type rejection, and missing file handling. Dockerfile.ci ships the prebuilt image used as the `image:` for kubernetes CI steps: rust stable, cargo-nextest, cargo-llvm-cov, sccache (configured via WFE_SCCACHE_* env), buildctl for in-cluster buildkitd, kubectl, tea for Gitea releases, and git. Published to `src.sunbeam.pt/studio/wfe-ci:latest`.
2026-04-07 19:09:26 +01:00
//! Tests for `whoami` and `logout` commands using a temp HOME with a fake token.
use chrono::Utc;
use std::path::PathBuf;
use std::sync::Mutex;
use wfectl::auth::{StoredToken, save_token};
use wfectl::commands::{logout, whoami};
use wfectl::config::Config;
use wfectl::output::OutputFormat;
/// Serialize HOME-mutating tests so they don't race.
static HOME_LOCK: Mutex<()> = Mutex::new(());
fn make_test_token(domain: &str) -> StoredToken {
StoredToken {
access_token: "ory_at_test".into(),
refresh_token: Some("ory_rt_test".into()),
// {"email":"alice@test.com","name":"Alice","groups":["admin","employee"]}
id_token: Some("h.eyJlbWFpbCI6ImFsaWNlQHRlc3QuY29tIiwibmFtZSI6IkFsaWNlIiwiZ3JvdXBzIjpbImFkbWluIiwiZW1wbG95ZWUiXX0.s".into()),
expires_at: Utc::now() + chrono::Duration::seconds(3600),
issuer: "https://auth.test.com/".into(),
domain: domain.into(),
}
}
fn with_temp_home<F: FnOnce(PathBuf)>(f: F) {
let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let tmp = tempfile::tempdir().unwrap();
let saved_home = std::env::var("HOME").ok();
unsafe { std::env::set_var("HOME", tmp.path()) };
f(tmp.path().to_path_buf());
if let Some(h) = saved_home {
unsafe { std::env::set_var("HOME", h) };
}
}
#[tokio::test]
async fn whoami_table_format_with_full_claims() {
with_temp_home(|_| {});
// Re-acquire the lock for the async section.
let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let tmp = tempfile::tempdir().unwrap();
let saved_home = std::env::var("HOME").ok();
unsafe { std::env::set_var("HOME", tmp.path()) };
let token = make_test_token("test.com");
save_token(&token).unwrap();
let cfg = Config {
server: "http://localhost".into(),
issuer: "https://auth.test.com/".into(),
default_format: Default::default(),
};
let args = whoami::WhoamiArgs { issuer: None };
whoami::run(args, &cfg, OutputFormat::Table).await.unwrap();
if let Some(h) = saved_home {
unsafe { std::env::set_var("HOME", h) };
}
}
#[tokio::test]
async fn whoami_json_format() {
let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let tmp = tempfile::tempdir().unwrap();
let saved_home = std::env::var("HOME").ok();
unsafe { std::env::set_var("HOME", tmp.path()) };
let token = make_test_token("test.com");
save_token(&token).unwrap();
let cfg = Config {
server: "http://localhost".into(),
issuer: "https://auth.test.com/".into(),
default_format: Default::default(),
};
let args = whoami::WhoamiArgs { issuer: None };
whoami::run(args, &cfg, OutputFormat::Json).await.unwrap();
if let Some(h) = saved_home {
unsafe { std::env::set_var("HOME", h) };
}
}
#[tokio::test]
async fn whoami_when_not_logged_in() {
let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let tmp = tempfile::tempdir().unwrap();
let saved_home = std::env::var("HOME").ok();
unsafe { std::env::set_var("HOME", tmp.path()) };
let cfg = Config {
server: "http://localhost".into(),
issuer: "https://auth.notlogged.in/".into(),
default_format: Default::default(),
};
let args = whoami::WhoamiArgs { issuer: None };
// Should not panic, just print a message.
whoami::run(args, &cfg, OutputFormat::Table).await.unwrap();
if let Some(h) = saved_home {
unsafe { std::env::set_var("HOME", h) };
}
}
#[tokio::test]
async fn whoami_with_explicit_issuer_arg() {
let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let tmp = tempfile::tempdir().unwrap();
let saved_home = std::env::var("HOME").ok();
unsafe { std::env::set_var("HOME", tmp.path()) };
let token = make_test_token("override.com");
save_token(&token).unwrap();
let cfg = Config::default();
let args = whoami::WhoamiArgs {
issuer: Some("https://auth.override.com/".into()),
};
whoami::run(args, &cfg, OutputFormat::Table).await.unwrap();
if let Some(h) = saved_home {
unsafe { std::env::set_var("HOME", h) };
}
}
#[tokio::test]
async fn logout_removes_token() {
let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let tmp = tempfile::tempdir().unwrap();
let saved_home = std::env::var("HOME").ok();
unsafe { std::env::set_var("HOME", tmp.path()) };
let token = make_test_token("test.com");
save_token(&token).unwrap();
let cfg = Config {
server: "http://localhost".into(),
issuer: "https://auth.test.com/".into(),
default_format: Default::default(),
};
let args = logout::LogoutArgs { issuer: None };
logout::run(args, &cfg).await.unwrap();
// Verify the token file is gone.
let path = wfectl::auth::token_path("test.com");
assert!(!path.exists());
if let Some(h) = saved_home {
unsafe { std::env::set_var("HOME", h) };
}
}
#[tokio::test]
async fn logout_when_not_logged_in_is_noop() {
let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let tmp = tempfile::tempdir().unwrap();
let saved_home = std::env::var("HOME").ok();
unsafe { std::env::set_var("HOME", tmp.path()) };
let cfg = Config {
server: "http://localhost".into(),
issuer: "https://auth.nothing.com/".into(),
default_format: Default::default(),
};
let args = logout::LogoutArgs { issuer: None };
logout::run(args, &cfg).await.unwrap();
if let Some(h) = saved_home {
unsafe { std::env::set_var("HOME", h) };
}
}
#[tokio::test]
async fn logout_with_explicit_issuer() {
let _g = HOME_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let tmp = tempfile::tempdir().unwrap();
let saved_home = std::env::var("HOME").ok();
unsafe { std::env::set_var("HOME", tmp.path()) };
let token = make_test_token("explicit.com");
save_token(&token).unwrap();
let cfg = Config::default();
let args = logout::LogoutArgs {
issuer: Some("https://auth.explicit.com/".into()),
};
logout::run(args, &cfg).await.unwrap();
if let Some(h) = saved_home {
unsafe { std::env::set_var("HOME", h) };
}
}