Files
wfe/wfectl/tests/auth_commands.rs
Sienna Meridian Satterwhite 0c239cd484 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

195 lines
6.1 KiB
Rust

//! 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) };
}
}